Skip to content
Destyler UI Destyler UI Destyler UI

Tour

A tour is an onboarding component used to guide users through a new product feature or series of steps. It is often used to boost feature discoverability or onboard new users by highlighting specific elements on the page.

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/tour @destyler/vue
Terminal window
      
        
npm install @destyler/tour @destyler/react
Terminal window
      
        
npm install @destyler/tour @destyler/svelte
Terminal window
      
        
npm install @destyler/tour @destyler/solid

Anatomy

Import all parts and piece them together.

<script setup lang="ts">
import * as tour from '@destyler/tour'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const [state, send] = useMachine(tour.machine({
id: useId(),
}))
const api = computed(() => tour.connect(state.value, send, normalizeProps))
const open = computed(() => api.value.open && api.value.step)
function start() {
api.value.start()
}
</script>
<template>
<div v-bind="api.getBackdropProps()" />
<div v-bind="api.getSpotlightProps()" />
<div v-bind="api.getPositionerProps()">
<div v-bind="api.getContentProps()">
<div v-bind="api.getArrowProps()">
<div v-bind="api.getArrowTipProps()" />
</div>
<p v-bind="api.getTitleProps()">
{{ api.step?.title }}
</p>
<div v-bind="api.getDescriptionProps()">
{{ api.step?.description }}
</div>
<div v-bind="api.getProgressTextProps()">
{{ api.getProgressText() }}
</div>
<button
v-for="action in api.step?.actions"
:key="action.label"
v-bind="api.getActionTriggerProps({ action })"
>
{{ action.label }}
</button>
<button v-bind="api.getCloseTriggerProps()">
</button>
</div>
</div>
</template>
import { normalizeProps, Portal, useMachine } from '@destyler/react'
import * as tour from '@destyler/tour'
import { useId } from 'react'
export default function Page() {
const [state, send] = useMachine(
tour.machine({
id: useId(),
}),
)
const api = tour.connect(state, send, normalizeProps)
return (
<>
<div {...api.getBackdropProps()} />
<div {...api.getSpotlightProps()} />
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<div {...api.getArrowProps()}>
<div {...api.getArrowTipProps()} />
</div>
<p {...api.getTitleProps()}>{api.step.title}</p>
<div {...api.getDescriptionProps()}>{api.step.description}</div>
<div {...api.getProgressTextProps()}>{api.getProgressText()}</div>
{api.step.actions.map(action => (
<button key={action.label} {...api.getActionTriggerProps({ action })}>
{action.label}
</button>
))}
<button {...api.getCloseTriggerProps()}>
</button>
</div>
</div>
</>
)
}
<script lang="ts">
import * as tour from "@destyler/tour";
import { useMachine, normalizeProps, portal } from "@destyler/svelte";
const id = $props.id()
const [state, send] = useMachine(tour.machine({
id: id,
}))
const api = $derived(tour.connect(state, send, normalizeProps))
</script>
<div {...api.getBackdropProps()}></div>
<div {...api.getSpotlightProps()}></div>
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<div {...api.getArrowProps()}>
<div {...api.getArrowTipProps()}></div>
</div>
<p {...api.getTitleProps()}>{api.step.title}</p>
<div {...api.getDescriptionProps()}>{api.step.description}</div>
{#each api.step.actions as action}
<button {...api.getActionTriggerProps({ action })}>{action.label}</button>
{/each}
<button {...api.getCloseTriggerProps()}>
</button>
</div>
</div>
import { normalizeProps, useMachine } from '@destyler/solid'
import * as tour from '@destyler/tour'
import { createMemo, createUniqueId, For, Show } from 'solid-js'
import { Portal } from 'solid-js/web'
export default function Tour() {
const id = createUniqueId()
const [state, send] = useMachine(tour.machine({ id }))
const api = createMemo(()=>tour.connect(state, send, normalizeProps))
return (
<>
<div {...api().getBackdropProps()} />
<div {...api().getSpotlightProps()} />
<div {...api().getPositionerProps()}>
<div {...api().getContentProps()}>
<div {...api().getArrowProps()}>
<div {...api().getArrowTipProps()} />
</div>
<p {...api().getTitleProps()}>{api().step!.title}</p>
<div {...api().getDescriptionProps()}>{api().step!.description}</div>
<For each={api().step?.actions}>
{action => <button {...api().getActionTriggerProps({ action })}>{action.label}</button>}
</For>
<button {...api().getCloseTriggerProps()}>
</button>
</div>
</div>
</>
)
}

Using step types

The tour machine supports different types of steps, allowing you to create a diverse and interactive tour experience. The available step types are defined in the StepType type:

  • "tooltip": Displays the step content as a tooltip, typically positioned near the target element.

  • "dialog": Shows the step content in a modal dialog centered on screen, useful for starting or ending the tour. This usually don’t have a target defined.

  • "floating": Presents the step content as a floating element, which can be positioned flexibly on the screen. This usually don’t have a target defined.

  • "wait": A special type that waits for a specific condition before proceeding to the next step.

const steps: tour.StepDetails[] = [
// Tooltip
{
id: "step-1",
type: "tooltip",
placement: "top-start",
target: () => document.querySelector("#target-1"),
title: "Tooltip Step",
description: "This is a tooltip step",
},
// Dialog
{
id: "step-2",
type: "dialog",
title: "Dialog Step",
description: "This is a dialog step",
},
// Floating
{
id: "step-3",
type: "floating",
placement: "top-start",
title: "Floating Step",
description: "This is a floating step",
},
// Wait
{
id: "step-4",
type: "wait",
title: "Wait Step",
description: "This is a wait step",
effect({ next }) {
// do something and go next
// you can also return a cleanup
},
},
]

Configuring actions

Every step supports a list of actions that are rendered in the step footer.Use the actions property to define each action.

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "dialog",
title: "Dialog Step",
description: "This is a dialog step",
actions: [
{ label: "Show me a tour!", action: "next" }
],
},
]

Changing tooltip placement

Use the placement property to define the placement of the tooltip.

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "tooltip",
placement: "top-start",
},
]

Hiding the arrow

Set arrow: false in the step property to hide the tooltip arrow. This is only useful for tooltip steps.

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "tooltip",
arrow: false,
},
]

Hiding the backdrop

Set backdrop: false in the step property to hide the backdrop. This applies to all step types except the wait step.

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "dialog",
backdrop: false,
},
]

Step Effects

Step effects are functions that are called before a step is opened. They are useful for adding custom logic to a step.

This function provides the following methods:

  • next(): Call this method to move to the next step.

  • show(): Call this method to show the current step.

  • update(details: StepDetails): Call this method to update the details of the current step (say, after data has been fetched).

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "tooltip",
effect({ next, show, update }) {
fetchData().then((res) => {
// update the step details
update({ title: res.title })
// then show show the step
show()
})
return () => {
// cleanup fetch data
}
},
},
]

Wait Steps

Wait steps are useful when you need to wait for a specific condition before proceeding to the next step.

Use the step effect function to perform an action and then call next() to move to the next step.

You cannot call show() in a wait step.

const steps: tour.StepDetails[] = [
{
id: "step-1",
type: "wait",
effect({ next }) {
const button = document.querySelector("#button")
const listener = () => next()
button.addEventListener("click", listener)
return () => button.removeEventListener("click", listener)
},
},
]

Tracking the lifecycle

As the tour is progressed, events are fired and you can track the lifecycle of the tour. Here’s are the events you can listen to:

  • onStepChange: Fires when the current step changes.

  • onStatusChange: Fires when the status of the tour changes.

const Lifecycle = () => {
const [state, send] = useMachine(
tour.machine({
steps: [],
onStepChange(details) {
// => { stepId: "step-1", stepIndex: 0, totalSteps: 3, complete: false, progress: 0 }
console.log(details)
},
onStatusChange(status) {
// => { status: "started" | "skipped" | "completed" | "dismissed" | "not-found" }
console.log(status)
},
}),
)
const api = tour.connect(state, send, normalizeProps)
}

Styling guide

Earlier, we mentioned that each Tooltip part has a data-part attribute added to them to select and style them in the DOM.

Prerequisites

Ensure the box-sizing is set to border-box for the means of measuring the tour target.

* {
box-sizing: border-box;
}

Ensure the body has a position of relative.

body {
position: relative;
}

Overview

Each tour part has a data-part attribute that can be used to style them in the DOM.

[data-scope="tour"][data-part="content"] {
/* styles for the content part */
}
[data-scope="tour"][data-part="positioner"] {
/* styles for the positioner part */
}
[data-scope="tour"][data-part="arrow"] {
/* styles for the arrow part */
}
[data-scope="tour"][data-part="title"] {
/* styles for the title part */
}
[data-scope="tour"][data-part="description"] {
/* styles for the description part */
}
[data-scope="tour"][data-part="progress-text"] {
/* styles for the progress text part */
}
[data-scope="tour"][data-part="action-trigger"] {
/* styles for the action trigger part */
}
[data-scope="tour"][data-part="backdrop"] {
/* styles for the backdrop part */
}

Step types

The tour component supports two types: dialog and floating. You can apply specific styles based on the tour type:

[data-scope="tour"][data-part="content"][data-type="dialog"] {
/* styles for content when step is dialog type */
}
[data-scope="tour"][data-part="content"][data-type="floating"] {
/* styles for content when step is floating type */
}
[data-scope="tour"][data-part="positioner"][data-type="dialog"] {
/* styles for positioner when step is dialog type */
}
[data-scope="tour"][data-part="positioner"][data-type="floating"] {
/* styles for positioner when step is floating type */
}

Placement Styles

For floating type tours, you can style based on the placement using the data-placement attribute:

[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="bottom"] {
/* styles for bottom placement */
}
[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="top"] {
/* styles for top placement */
}
[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="start"] {
/* styles for start placement */
}
[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="end"] {
/* styles for end placement */
}

Methods and Properties

Machine Context

The tour machine exposes the following context properties:

ids
Partial<{ content: string; title: string; description: string; positioner: string; backdrop: string; arrow: string; }>
The ids of the elements in the tour. Useful for composition.
steps
StepDetails[]
The steps of the tour
stepId
string
The id of the currently highlighted step
onStepChange
(details: StepChangeDetails) => void
Callback when the highlighted step changes
onStatusChange
(details: StatusChangeDetails) => void
Callback when the tour is opened or closed
closeOnInteractOutside(default: true)
boolean
Whether to close the tour when the user clicks outside the tour
closeOnEscape(default: true)
boolean
Whether to close the tour when the user presses the escape key
keyboardNavigation(default: true)
boolean
Whether to allow keyboard navigation (right/left arrow keys to navigate between steps)
preventInteraction(default: false)
boolean
Prevents interaction with the rest of the page while the tour is open
spotlightOffset(default: "{ x: 10, y: 10 }")
Point
The offsets to apply to the spotlight
spotlightRadius(default: 4)
number
The radius of the spotlight clip path
translations
IntlTranslations
The translations for the tour
dir(default: "ltr")
"ltr" | "rtl"
The document's text/writing direction.
id
string
The unique identifier of the machine.
getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the component
onFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the component
onInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component

Machine API

The tour api exposes the following methods:

open
boolean
Whether the tour is open
totalSteps
number
The total number of steps
stepIndex
number
The index of the current step
step
StepDetails
The current step details
hasNextStep
boolean
Whether there is a next step
hasPrevStep
boolean
Whether there is a previous step
firstStep
boolean
Whether the current step is the first step
lastStep
boolean
Whether the current step is the last step
addStep
(step: StepDetails) => void
Add a new step to the tour
removeStep
(id: string) => void
Remove a step from the tour
updateStep
(id: string, stepOverrides: Partial<StepDetails>) => void
Update a step in the tour with partial details
setSteps
(steps: StepDetails[]) => void
Set the steps of the tour
setStep
(id: string) => void
Set the current step of the tour
start
(id?: string) => void
Start the tour at a specific step (or the first step if not provided)
isValidStep
(id: string) => boolean
Check if a step is valid
isCurrentStep
(id: string) => boolean
Check if a step is visible
next
() => void
Move to the next step
prev
() => void
Move to the previous step
getProgressText
() => string
Returns the progress text
getProgressPercent
() => number
Returns the progress percent

Data Attributes

Backdrop

attribute
description
data-scope
tour
data-part
backdrop
data-state
"open" | "closed"
data-type
The type of the item

Positioner

attribute
description
data-scope
tour
data-part
positioner
data-type
The type of the item
data-placement
The placement of the positioner

Content

attribute
description
data-scope
tour
data-part
content
data-state
"open" | "closed"
data-type
The type of the item
data-placement
The placement of the content
data-step

Title

attribute
description
data-scope
tour
data-part
title
data-placement
The placement of the title

Description

attribute
description
data-scope
tour
data-part
description
data-placement
The placement of the description

Close Trigger

attribute
description
data-scope
tour
data-part
close-trigger
data-type
The type of the item

Action Trigger

attribute
description
data-scope
tour
data-part
action-trigger
data-type
The type of the item
data-disabled
Present when disabled