Tour
The <x-wirekit::tour> component creates a step-by-step product tour overlay. Each step is positioned near a target element using Floating UI and includes navigation controls (Back / Next / Finish). The tour overlays a semi-transparent backdrop and can be dismissed with the Escape key.
Usage
The demo below shows the tour attached to three target elements in a mock dashboard frame. Click Start Tour to run through three steps that appear at different positions. Click Start Tour again to restart.
Welcome
Create a Project
Settings
Viewport-Level Rendering
The tour overlay and step panels are rendered directly in <body> via Alpine's x-teleport — the same pattern used by Modal, Drawer, and other overlay components. This guarantees correct position: fixed positioning regardless of ancestor CSS (transform, will-change, contain, etc.) that would otherwise create a containing block and break Floating UI's coordinate math.
Placement Suffixes
The base placements (top, bottom, left, right) center the popover on the target. Add -start or -end to align the popover's edge with the target's edge:
top-start— popover above the target, left edges alignedbottom-end— popover below the target, right edges aligned
This is useful for targets near container edges where centering would push the popover out of bounds.
Opening Without Flicker
Each <x-wirekit::tour.step> ships a CSS rule ([data-wk-tour-step] { left: -9999px; top: -9999px }) in dist/wirekit.css that parks the panel off-screen until Floating UI computes its real position and writes left/top as inline styles. Inline styles outrank stylesheet rules, so the computed values immediately override the -9999px fallback — the user never sees the panel flash at (0, 0).
Starting the Tour
The tour starts hidden. Dispatch a custom event to start it:
{{-- Alpine --}}
<button x-on:click="$dispatch('wirekit-tour-start-onboarding')">
Start Tour
</button>
{{-- Livewire (server-side) --}}
$this->dispatch('wirekit-tour-start-onboarding');
{{-- Vanilla JS --}}
<script>
window.dispatchEvent(new CustomEvent('wirekit-tour-start-onboarding'));
</script>
The event name follows the pattern wirekit-tour-start-{name}, where {name} is the name prop on the tour component.
Step Placement
Each step is positioned relative to its target element using Floating UI. The placement prop controls the preferred position:
<x-wirekit::tour.step :index="0" target="#my-element" placement="top">
Step content above the target element.
</x-wirekit::tour.step>
<x-wirekit::tour.step :index="1" target=".sidebar-nav" placement="right">
Step content to the right of the target.
</x-wirekit::tour.step>
Available placements: top, right, bottom (default), left, plus their -start / -end edge-aligned variants (e.g. top-start, bottom-end). Floating UI will automatically flip to the opposite side if there is not enough space.
Auto-Scroll
When a step becomes active, the target element is scrolled into view using scrollIntoView({ behavior: 'smooth', block: 'center' }). This ensures the user always sees the element being highlighted.
Navigation
Each step renders built-in navigation controls:
- Back button — shown from step 2 onward; calls
prev()to go back one step - Next button — advances to the next step; label changes to Finish on the last step
- Progress text — displays "Step X of Y" (also announced to screen readers)
Keyboard
| Key | Action |
|---|---|
| Escape | Dismiss the tour at any point |
| Tab | Move focus between Back, Next, and other focusable elements inside the step |
Behavior
- Teleport to body — overlay, backdrop, and steps are rendered in
<body>viax-teleport, escaping ancestor containing blocks - Overlay backdrop — a fixed, full-screen overlay with
z-indexfrom--z-wk-modalsits behind the steps - Floating UI positioning — each step is positioned 12px away from its target element
- Step counting — total steps are counted from
[data-wk-tour-step]elements in the DOM at init - Visibility toggling — step content uses
x-show="active"withx-cloakso the DOM is present but hidden until the tour starts - Dismiss — calling
dismiss()or pressing Escape ends the tour immediately, resetting to step 0
Props
<x-wirekit::tour>
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
'tour' |
Unique tour identifier — used in the start event name |
scope |
string|null |
null |
Scoped personalization key |
<x-wirekit::tour.step>
| Prop | Type | Default | Description |
|---|---|---|---|
level |
int |
3 |
Heading level for the title, 1–6. The default is what it has always rendered — a step's title inside the dialog — so nothing moves unless you set it. Set it to match the surrounding outline: a skipped level is what heading-order reports, and that rule sits in axe's best-practice tag rather than the WCAG tags most suites run, so it is invisible to an axe sweep and visible in Lighthouse. |
target |
string|null |
null |
CSS selector for the element this step points to (e.g. #my-button, .sidebar) |
placement |
string |
'bottom' |
Preferred position relative to the target: top, right, bottom, left |
index |
int|null |
null |
Explicit zero-based step number. Left null, steps are auto-assigned in document order — pass an explicit index only to reorder. |
scope |
string|null |
null |
Scoped personalization key |
Named Slots
The tour.step component supports:
title— optional named slot for the step heading (rendered as<h3>)- Default slot — the step body content
<x-wirekit::tour.step :index="0" target="#btn">
<x-slot:title>Step Heading</x-slot:title>
Body text goes here.
</x-wirekit::tour.step>
Sub-Components
| Component | Purpose |
|---|---|
tour.step |
Individual step popup with title, content, and navigation controls |
Accessibility
- Steps:
role="dialog",aria-modal="true"— the scrim covers the whole viewport, so the page behind a step is unavailable while the tour runs - Each step is named by its
titleslot viaaria-labelledby; a step without a title falls back toaria-label="Tour step N" - Pass
aria-labelon the step to override both. A name written at the call site is the most specific instruction there is, so it wins over the title slot and over the generated fallback — useful when the announcement should be shorter or more telling than the visible heading. An emptyaria-label=""is ignored (it would name the dialog nothing), and the title slot still renders either way - Backdrop:
aria-hidden="true"(decorative overlay) - Live region:
aria-live="polite"announces "Step X of Y" on every step change - Navigation buttons have visible text labels
- Focus ring visible on all interactive elements within steps
- Focus moves onto the step panel itself when the tour starts and again on every step change, so the step's title and body are announced ahead of its Back / Next controls
- Focus stays inside the open step — the page behind the scrim is not reachable with Tab
- Finishing or dismissing the tour returns focus to the element that started it
Body scroll is intentionally not locked, which is the one way the tour differs from the modal and drawer overlays: a tour scrolls each target element into view, and a scroll lock would prevent exactly that.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move between the controls of the current step; focus wraps within the step |
Shift + Tab |
Move backward through the same controls |
Enter / Space |
Trigger the focused action button (Next / Back / Finish) |
Escape |
Dismiss the tour and return focus to the element that started it |
Pitfalls
- Don't trigger a tour on every page load. WCAG 2.2.2 (Pause, Stop, Hide) requires a way to dismiss recurring auto-content. Show only on first visit; persist the dismissal in
localStorage. - Don't anchor tour steps to elements that may not exist. If the target is conditionally rendered, the step renders floating. Use
:stepscallbacks that filter to elements present at runtime.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Step font family |
--font-wk-heading-weight |
Step title weight |
--text-wk-sm / --text-wk-md / --text-wk-lg |
Body / step indicator / title font size |
--color-wk-text |
Step body text |
--color-wk-text-muted |
Step indicator + secondary text |
--color-wk-accent |
Primary action button background |
--color-wk-accent-fg |
Primary action button text |
--color-wk-accent-hover |
Primary action button hover |
--color-wk-bg-elevated |
Step panel background |
--color-wk-bg-subtle |
Secondary action button hover |
--color-wk-border |
Step panel border |
--color-wk-overlay |
Backdrop scrim color |
--color-wk-ring |
Button focus ring |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-sm / --radius-wk-md / --radius-wk-lg |
Step + button border radius |
--shadow-wk-lg |
Step panel drop shadow |
--gap-wk-sm |
Footer-button gap |
--padding-wk-x-sm / --padding-wk-x-md / --padding-wk-y-xs / --padding-wk-y-md |
Step + button padding |
--z-wk-modal |
Stacking context |
Personalization
Override classes globally via WireKit::personalize():
use Pushery\WireKit\WireKit;
WireKit::personalize('tour.step', [
'base' => 'fixed z-50 w-96 bg-white border rounded-xl shadow-2xl p-6',
]);
Further Reading
- WAI-ARIA Dialog Pattern — dialog semantics used by each tour step
- Floating UI — positioning library used to anchor steps to target elements
- MDN:
aria-live— live region for step announcements