Hooks

useTour

A headless controller for the Tour component. Pure reactive state with no $effect, safe to create conditionally, and SSR-safe. Use it when the tour state must outlive a single component: multi-page onboarding, persistence across reloads, or driving the tour from anywhere in the app. For a simple single-page tour, bind:api on the component is enough.

Basic Usage

Create the controller with useTour(options) and hand it to the component via controller. The component then never creates its own state.

isActive: false, step: 0 / 2, last event: none yet

<script lang="ts">
  import { Tour, useTour, Button } from 'sv5ui';
  import type { TourStep } from 'sv5ui';

  const steps: TourStep[] = [
    { target: '#inbox', title: 'Inbox' },
    { target: '#compose', title: 'Compose' }
  ];

  const tour = useTour({
    steps,
    onComplete: () => console.log('finished!'),
    onSkip: (index) => console.log('left at step', index)
  });
</script>

<Button label="Start" onclick={() => tour.start()} />
<Tour {steps} controller={tour} />

The Controller

The returned TourController mixes imperative methods with reactive read-only state. There is no bind:open: the controller is the single source of truth.

These buttons live outside the component and call the controller directly. Start the Basic demo tour above, then drive it from here. isActive: false. The same controller type is what bind:api exposes on the component.

<script lang="ts">
  const tour = useTour({ steps });
</script>

<!-- Imperative API -->
<Button onclick={() => tour.start()}>Start</Button>
<Button onclick={() => tour.start('billing')}>Start at step "billing"</Button>
<Button onclick={() => tour.goTo(2)}>Jump to step 3</Button>
<Button onclick={() => tour.stop()}>Stop</Button>

<!-- Reactive state -->
{#if tour.isActive}
  <p>Step {tour.currentIndex + 1} of {tour.totalSteps}</p>
{/if}

Multi-page Tours with persist

persist stores the open state and current step in localStorage (or sessionStorage), so the tour resumes after a reload or SPA navigation. Steps carry a route as metadata; your onStepChange calls goto(), and waitForTarget covers the render gap after navigation. Render one <Tour> in your root layout so it exists on every page.

Step A Step B

This demo persists to sessionStorage: start it, reload the page, and it resumes on the same step. Cross-route resumption works the same way; the recipe above adds the router wiring.

<script lang="ts">
  import { goto } from '$app/navigation';

  // persist keeps the open state + current step in storage,
  // so the tour survives reloads and SPA navigations.
  const tour = useTour({
    steps,
    persist: { key: 'onboarding-tour', storage: 'session' },
    onStepChange: (index) => {
      const route = steps[index]?.route;
      if (route) goto(route);
    }
  });
</script>

<!-- Steps carry a route as metadata; the hook never navigates
     itself. Your onStepChange drives the router. -->
const steps: TourStep[] = [
  { target: '#dashboard-kpis', title: 'Your KPIs', route: '/dashboard' },
  { target: '#settings-form', title: 'Settings', route: '/settings', waitForTarget: true }
];

Live Demo: A Tour Across These Docs

This site ships the exact pattern above. The controller lives in a shared .svelte.ts module, the <Tour> is rendered once in the docs layout, and the button below starts a tour that navigates from this page to the Tour and Header pages and back to a centered outro. Reload mid-tour to see persist resume it.

Watch the URL: step 2 navigates to /docs/components/tour and step 3 to /docs/components/header.

// src/lib/docs-tour.svelte.ts - shared module (.svelte.ts for runes)
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { useTour } from 'sv5ui';
import type { TourStep } from 'sv5ui';

export const docsTourSteps: TourStep[] = [
  {
    route: '/docs/hooks/use-tour',
    target: '#docs-tour-live-start',
    title: 'A real multi-page tour'
  },
  {
    route: '/docs/components/tour',
    target: '#tour-page-title',
    title: 'It navigated for real',
    waitForTarget: 3000 // covers the render gap after goto()
  },
  {
    route: '/docs/components/header',
    target: '#header-page-title',
    title: 'Any route works',
    waitForTarget: 3000
  },
  { title: 'It also survives reloads' } // centered outro
];

function syncRoute(index: number) {
  if (!browser) return;
  const route = docsTourSteps[index]?.route;
  if (route && window.location.pathname !== route) goto(route);
}

export const docsTour = useTour({
  steps: docsTourSteps,
  persist: { key: 'sv5ui-docs-multi-page-tour', storage: 'session' },
  onStepChange: (index) => syncRoute(index)
});

// Root (or section) layout: render once so it exists on every page
// <Tour steps={docsTourSteps} controller={docsTour} />

// Anywhere in the app:
// <Button onclick={() => docsTour.start()}>Start</Button>

When You Do Not Need the Hook

For a tour confined to one page, skip the hook and bind api instead: the component creates and owns the controller internally.

Target

See the Tour component page for full examples of this style.

<!-- For a single-page tour you do not need the hook:
     let the component own the controller and bind api. -->
<script lang="ts">
  import type { TourController } from 'sv5ui';

  let api = $state<TourController>();
</script>

<Button label="Start tour" onclick={() => api?.start()} />
<Tour {steps} bind:api />

Options

OptionTypeDefault
stepsTourStep[]-
defaultStepnumber0
persistboolean | { key?, storage? }false
onStart() => void-
onComplete() => void-
onSkip(index: number) => void-
onStepChange(index, prev) => void-

Return (TourController)

PropertyType-
start(step?: number | string) => void-
stop() => void-
next / prev() => void-
goTo(step: number | string) => void-
isActiveboolean-
currentIndexnumber-
totalStepsnumber-
hasPrev / hasNext / isFirst / isLastboolean-
currentStepDataTourStep | undefined-