Tour
A neo-brutalist step-by-step guided tour component. Uses a canvas cutout mask to highlight target elements with popover panels for step descriptions. Supports four-directional placement, keyboard navigation, and viewport boundary constraints to help users quickly learn interface features.
Demo
Installation
pnpm dlx brutx-vue@latest add tourUsage
Basic Usage
Define a steps array and control the tour's visibility and current step via v-model:open and v-model:current:
<script setup>
import { ref } from 'vue'
import { Tour } from 'brutx-ui-vue'
const isOpen = ref(true)
const current = ref(0)
const steps = [
{
target: '#step-nav',
title: 'Navigation',
description: 'This is the main navigation area with links to key sections.',
placement: 'bottom',
},
{
target: '#step-search',
title: 'Search',
description: 'Use the search bar to quickly find what you need.',
placement: 'right',
},
{
target: '#step-profile',
title: 'Profile',
description: 'Click your avatar to manage your profile settings.',
placement: 'left',
},
]
</script>
<template>
<Tour v-model:open="isOpen" v-model:current="current" :steps="steps" />
</template>Custom Placement and Mask
Each step can independently set the popover direction (placement) and whether to show the mask (mask). When not specified on a step, the component-level mask prop is inherited (defaults to true).
<script setup>
import { ref } from 'vue'
import { Tour } from 'brutx-ui-vue'
const isOpen = ref(true)
const current = ref(0)
const steps = [
{
target: '#hero-section',
title: 'Welcome',
description: 'This is the hero section.',
placement: 'top',
mask: true,
},
{
target: '#sidebar',
title: 'Sidebar',
description: 'The sidebar has no mask overlay.',
placement: 'right',
mask: false,
},
]
</script>
<template>
<Tour v-model:open="isOpen" v-model:current="current" :steps="steps" />
</template>Listening to Events
Respond to user actions via the @skip, @finish, and @close events:
<script setup>
import { ref } from 'vue'
import { Tour } from 'brutx-ui-vue'
const isOpen = ref(true)
const current = ref(0)
const steps = [
{ target: '#feature-a', title: 'Feature A', description: 'Learn about Feature A.' },
{ target: '#feature-b', title: 'Feature B', description: 'Learn about Feature B.' },
]
const onSkip = () => {
console.log('User skipped the tour')
}
const onFinish = () => {
console.log('User completed all steps')
}
const onClose = () => {
console.log('Tour closed')
}
</script>
<template>
<Tour
v-model:open="isOpen"
v-model:current="current"
:steps="steps"
@skip="onSkip"
@finish="onFinish"
@close="onClose"
/>
</template>Custom Scroll Behavior
Customize how the target element scrolls into view via scrollIntoViewOptions:
<script setup>
import { ref } from 'vue'
import { Tour } from 'brutx-ui-vue'
const isOpen = ref(true)
const current = ref(0)
const steps = [
{ target: '#section-1', title: 'Section 1', description: 'Content at the top of the page.' },
{ target: '#section-2', title: 'Section 2', description: 'Requires scrolling down to see.' },
]
</script>
<template>
<Tour
v-model:open="isOpen"
v-model:current="current"
:steps="steps"
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
/>
</template>Data Types
TourStep
interface TourStep {
target: string | HTMLElement
title?: string
description?: string
placement?: 'top' | 'bottom' | 'left' | 'right'
mask?: boolean
}| Field | Type | Default | Description |
|---|---|---|---|
target | string | HTMLElement | — | Target element as a CSS selector string or DOM element reference (required) |
title | string | — | Step title |
description | string | — | Step description text |
placement | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Popover direction relative to the target element |
mask | boolean | — | Whether to show the mask for this step; inherits from the component-level mask prop when not specified |
Exported Types
import type { TourStep, TourProps } from 'brutx-ui-vue'API Reference
| Name | Type | Default | Description |
|---|---|---|---|
stepsRequired | TourStep[] | — | Provides ordered tour steps. Each step targets an element by CSS selector or HTMLElement and may include a title, description, placement, and mask setting. |
DetailsDeclared typeTourStep[] Referenced types TourStep Type definition — TourStepexport interface TourStep {
target: string | HTMLElement
title?: string
description?: string
placement?: 'top' | 'bottom' | 'left' | 'right'
mask?: boolean
}Default value details
Sourcepackages/ui/src/components/tour/types.ts | |||
mask | boolean | undefined | true | Controls whether the mask is shown outside the current target. It defaults to true and can be overridden by the current TourStep.mask. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tour/types.ts | |||
scrollIntoViewOptions | ScrollIntoViewOptions | undefined | undefined | Sets the options passed to scrollIntoView before showing a step. When omitted, block: center and inline: nearest are used. |
DetailsDeclared typeScrollIntoViewOptions | undefined Default value details
Sourcepackages/ui/src/components/tour/types.ts | |||
current | number | undefined | 0 | The zero-based current tour step, synchronized through v-model:current and defaulting to the first step. |
DetailsDeclared typenumber | undefined Default value details
Sourcepackages/ui/src/components/tour/Tour.vue | |||
open | boolean | undefined | true | Controls whether the tour is open, synchronized through v-model:open and defaulting to open. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tour/Tour.vue | |||
| Name | Signature or type | Description |
|---|---|---|
@close | [] | Emitted when the user skips or finishes the tour and it closes. |
DetailsDeclared type[] Sourcepackages/ui/src/components/tour/Tour.vue | ||
@update:open | [value: boolean] | Emits the new boolean open state when visibility changes so v-model:open can be synchronized. |
DetailsDeclared type[value: boolean] Literal values
Sourcepackages/ui/src/components/tour/Tour.vue | ||
@finish | [] | Emitted when the user chooses Finish on the final step. |
DetailsDeclared type[] Sourcepackages/ui/src/components/tour/Tour.vue | ||
@update:current | [value: number] | Emits the new index when the current step changes so v-model:current can be synchronized. |
DetailsDeclared type[value: number] Sourcepackages/ui/src/components/tour/Tour.vue | ||
@skip | [] | Emitted when the user chooses to skip the tour. |
DetailsDeclared type[] Sourcepackages/ui/src/components/tour/Tour.vue | ||
Accessibility
- Keyboard: Press
Escapeto skip the tour, pressEnterto advance to the next step or finish the tour - ARIA Attributes: Step title and description in the popover panel are rendered with semantic markup; buttons include accessible text labels
- Focus Management: Buttons inside the popover panel are focusable while the tour is open; focus is released when the tour closes
- Viewport Constraints: The popover panel is automatically constrained within the viewport safe area and will not overflow screen edges
FAQ
Q: What happens if the target element is outside the visible area?
A: The Tour automatically calls scrollIntoView when switching steps to scroll the target element to the center of the viewport. You can customize the scroll behavior via the scrollIntoViewOptions prop, for example { behavior: 'smooth', block: 'start' } for smooth scrolling to the top.
Q: The mask position is incorrect after the target element resizes.
A: The Tour internally uses ResizeObserver to monitor target element size changes and automatically recalculates the mask cutout area and popover position. Global scroll and resize event listeners are also registered to prevent positioning drift caused by page scrolling or window resizing. All listeners are automatically cleaned up when the component unmounts.
Q: How do I use a DOM element reference instead of a CSS selector?
A: The target prop accepts an HTMLElement instance directly. In Vue, you can use a template ref to obtain the element and pass it to target:
<script setup>
import { ref } from 'vue'
import { Tour } from 'brutx-ui-vue'
const targetEl = ref(null)
const isOpen = ref(true)
const current = ref(0)
const steps = [{ target: targetEl, title: 'Title', description: 'Description' }]
</script>
<template>
<div ref="targetEl">Target element</div>
<Tour v-model:open="isOpen" v-model:current="current" :steps="steps" />
</template>Q: How do I run custom logic after the last step?
A: Listen to the @finish event. It is emitted when the user completes all steps and clicks the "Finish" button. The @close event is also emitted in both skip and finish scenarios, making it suitable for unified cleanup logic after the tour ends.