Skip to main content
WireKit
Copy for LLM

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.

Tour Setup
+ Create
Dashboard
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 aligned
  • bottom-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.

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> via x-teleport, escaping ancestor containing blocks
  • Overlay backdrop — a fixed, full-screen overlay with z-index from --z-wk-modal sits 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" with x-cloak so 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 title slot via aria-labelledby; a step without a title falls back to aria-label="Tour step N"
  • Pass aria-label on 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 empty aria-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 :steps callbacks 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

Was this page helpful?

Voting requires cookies or local storage. What we store