Skip to content

useTour ​

A guided tour. The page defines the steps and starts it; one overlay, mounted once, draws whichever tour is running.

Open in demo

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 ​

MemberTypeDescription
start() => voidMakes this tour the active one, at step 0.
end() => voidTears the overlay down without marking it done.
reset() => voidForgets that this tour was completed.
isCompletedComputedRef<boolean>Whether this id has been finished or skipped before.

TourStep ​

KeyTypeDescription
targetstringA CSS selector for the element to cut out of the dimmed page.
titlestringThe step's heading.
descriptionstringThe 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 by id, so it survives a reload.
  • One tour runs at a time. start() replaces whatever was active.
  • target is 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.