useTour
A guided tour. The page defines the steps and starts it; one overlay, mounted once, draws whichever tour is running.
vue
<script setup lang="ts">
import type { TourStep } from '@shared/composables/useTour'
import { Button } from '@shared/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'
import { Input } from '@shared/components/ui/input'
import { useTour, useTourController } from '@shared/composables/useTour'
const STEPS: TourStep[] = [
{
target: '[data-tour="invoice-search"]',
title: 'Find an invoice',
description: 'Search by number, customer or amount.',
position: 'bottom',
},
{
target: '[data-tour="invoice-new"]',
title: 'Raise a new one',
description: 'This always creates an invoice in the section you are in.',
position: 'bottom',
},
{
target: '[data-tour="invoice-total"]',
title: 'Watch the total',
description: 'It follows your filters, so what you see is what you get.',
position: 'top',
},
]
const { start, reset, isCompleted } = useTour({ id: 'docs-usetour-demo', steps: STEPS })
const { isActive, stepIndex, isLast } = useTourController()
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<Button size="sm" @click="start()">
Start the tour
</Button>
<Button size="sm" variant="outline" @click="reset()">
Mark it unseen
</Button>
<span class="sa-text-sm text-muted-foreground">
{{ isCompleted ? 'Completed' : 'Not completed yet' }}
</span>
</div>
<Card>
<CardHeader>
<CardTitle>Invoices</CardTitle>
</CardHeader>
<CardContent class="flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<Input data-tour="invoice-search" placeholder="Search invoices" class="max-w-xs" />
<Button data-tour="invoice-new" size="sm">
New invoice
</Button>
</div>
<p data-tour="invoice-total" class="sa-text-2xl font-semibold tabular-nums">
$48,120 outstanding
</p>
</CardContent>
</Card>
<p class="sa-text-sm text-muted-foreground">
Controller: active {{ isActive }} · step {{ stepIndex + 1 }} of {{ STEPS.length }} · last {{ isLast }}
</p>
</div>
</template>Import
ts
import { useTour, useTourController } from '@shared/composables/useTour'Signature
ts
function useTour(config: TourConfig): {
start: () => void
end: () => void
reset: () => void
isCompleted: ComputedRef<boolean>
}
interface TourConfig {
id: string
steps: TourStep[]
}
interface TourStep {
target: string
title: string
description: string
position?: 'top' | 'bottom' | 'left' | 'right'
}Returns
| Member | Type | Description |
|---|---|---|
start | () => void | Makes this tour the active one, at step 0. |
end | () => void | Tears the overlay down without marking it done. |
reset | () => void | Forgets that this tour was completed. |
isCompleted | ComputedRef<boolean> | Whether this id has been finished or skipped before. |
TourStep
| Key | Type | Description |
|---|---|---|
target | string | A CSS selector for the element to cut out of the dimmed page. |
title | string | The step's heading. |
description | string | The line under it. |
position | 'top' | 'bottom' | 'left' | 'right' | Which side the card sits on. |
Example
vue
<script setup lang="ts">
import { useTour } from '@shared/composables/useTour'
import { onMounted } from 'vue'
const { start, reset, isCompleted } = useTour({
id: 'invoices-intro',
steps: [
{
target: '#new-invoice',
title: 'Start here',
description: 'Every invoice begins with this button.',
position: 'bottom',
},
{
target: '#filters',
title: 'Narrow the list',
description: 'Filter by status, customer or date.',
position: 'right',
},
{
target: '#export',
title: 'Take it with you',
description: 'Export what you are looking at as CSV or Excel.',
position: 'left',
},
],
})
onMounted(() => {
if (!isCompleted.value)
start()
})
</script>
<template>
<div class="flex items-center gap-3">
<button id="new-invoice" type="button">
New invoice
</button>
<button id="filters" type="button">
Filters
</button>
<button id="export" type="button">
Export
</button>
<button type="button" @click="reset(); start()">
Show me again
</button>
</div>
</template>Notes
- Completing and ending are different on purpose. Finishing the last step, or pressing Skip, marks the tour done.
end()only tears the overlay down. It runs automatically on scope dispose, so navigating away mid-tour leaves no spotlight pointing at DOM that is gone — and does not permanently suppress a tour the reader never actually saw. - Completion is persisted under
sa-tours, keyed byid, so it survives a reload. - One tour runs at a time.
start()replaces whatever was active. targetis queried when the step renders, so the element has to exist by then. A selector matching nothing leaves the step with nothing highlighted.reset()removes just this tour's key, leaving other tours' completion alone.
useTourController
What the overlay itself uses. Reach for it only if you are replacing TourSpotlight.
ts
function useTourController(): {
activeTour: Readonly<Ref<TourConfig | null>>
currentStep: ComputedRef<TourStep | null>
stepIndex: Readonly<Ref<number>>
isActive: ComputedRef<boolean>
isFirst: ComputedRef<boolean>
isLast: ComputedRef<boolean>
next: () => void
prev: () => void
skip: () => void
}next() on the last step marks the tour complete and closes it. skip() does the same from anywhere. prev() stops at step 0.
Related
- Tour spotlight — the overlay component.
useShortcuts— the other half of onboarding.