Tour
A tour guides users through product features with step-by-step overlays.
Features
- Supports different step types such as "dialog", "floating", "tooltip" or "wait".
- Supports customizable content per step.
- Wait steps for waiting for a specific selector to appear on the page before showing the next step.
- Flexible positioning of the tour dialog per step.
- Progress tracking shows users their progress through the tour.
Installation
Install the tour package:
npm install @zag-js/tour @zag-js/react # or yarn add @zag-js/tour @zag-js/react
npm install @zag-js/tour @zag-js/solid # or yarn add @zag-js/tour @zag-js/solid
npm install @zag-js/tour @zag-js/vue # or yarn add @zag-js/tour @zag-js/vue
npm install @zag-js/tour @zag-js/svelte # or yarn add @zag-js/tour @zag-js/svelte
Anatomy
Check the tour anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the tour package:
import * as tour from "@zag-js/tour"
These are the key exports:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Then use the framework integration helpers:
import * as tour from "@zag-js/tour" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId } from "react" function Tour() { const service = useMachine(tour.machine, { id: useId(), steps }) const api = tour.connect(service, normalizeProps) return ( <div> <div> <button onClick={() => api.start()}>Start Tour</button> <div id="step-1">Step 1</div> </div> {api.step && api.open && ( <Portal> {api.step.backdrop && <div {...api.getBackdropProps()} />} <div {...api.getSpotlightProps()} /> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {api.step.arrow && ( <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 && ( <div> {api.step.actions.map((action) => ( <button key={action.label} {...api.getActionTriggerProps({ action })} > {action.label} </button> ))} </div> )} <button {...api.getCloseTriggerProps()}>X</button> </div> </div> </Portal> )} </div> ) } const steps: tour.StepDetails[] = [ { type: "dialog", id: "start", title: "Ready to go for a ride", description: "Let's take the tour component for a ride and have some fun!", actions: [{ label: "Let's go!", action: "next" }], }, { id: "logic", title: "Step 1", description: "This is the first step", target: () => document.querySelector("#step-1"), placement: "bottom", actions: [ { label: "Prev", action: "prev" }, { label: "Next", action: "next" }, ], }, { type: "dialog", id: "end", title: "Amazing! You got to the end", description: "Like what you see? Now go ahead and use it in your project.", actions: [{ label: "Finish", action: "dismiss" }], }, ]
import * as tour from "@zag-js/tour" import { useMachine, normalizeProps } from "@zag-js/solid" import { For, Show, createMemo, createUniqueId } from "solid-js" import { Portal } from "solid-js/web" function Tour() { const service = useMachine(tour.machine, { id: createUniqueId(), steps }) const api = createMemo(() => tour.connect(service, normalizeProps)) return ( <div> <div> <button onClick={() => api().start()}>Start Tour</button> <div id="step-1">Step 1</div> </div> <Show when={api().open && api().step}> <Portal> <Show when={api().step.backdrop}> <div {...api().getBackdropProps()} /> </Show> <div {...api().getSpotlightProps()} /> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}> <Show when={api().step.arrow}> <div {...api().getArrowProps()}> <div {...api().getArrowTipProps()} /> </div> </Show> <p {...api().getTitleProps()}>{api().step.title}</p> <div {...api().getDescriptionProps()}> {api().step.description} </div> <div> <For each={api().step.actions}> {(action) => ( <button {...api().getActionTriggerProps({ action })}> {action.label} </button> )} </For> </div> <button {...api().getCloseTriggerProps()}>X</button> </div> </div> </Portal> </Show> </div> ) } const steps: tour.StepDetails[] = [ { type: "dialog", id: "start", title: "Ready to go for a ride", description: "Let's take the tour component for a ride and have some fun!", actions: [{ label: "Let's go!", action: "next" }], }, { id: "logic", title: "Step 1", description: "This is the first step", target: () => document.querySelector("#step-1"), placement: "bottom", actions: [ { label: "Prev", action: "prev" }, { label: "Next", action: "next" }, ], }, { type: "dialog", id: "end", title: "Amazing! You got to the end", description: "Like what you see? Now go ahead and use it in your project.", actions: [{ label: "Finish", action: "dismiss" }], }, ]
<script setup lang="ts"> import * as tour from "@zag-js/tour" import { normalizeProps, useMachine } from "@zag-js/vue" import { useId, computed, Teleport } from "vue" const steps: tour.StepDetails[] = [ { type: "dialog", id: "start", title: "Ready to go for a ride", description: "Let's take the tour component for a ride and have some fun!", actions: [{ label: "Let's go!", action: "next" }], }, { type: "dialog", id: "logic", title: "Statechart", description: `As an engineer, you'll learn about the internal statechart that powers the tour.`, actions: [ { label: "Prev", action: "prev" }, { label: "Next", action: "next" }, ], }, { type: "dialog", id: "end", title: "Amazing! You got to the end", description: "Like what you see? Now go ahead and use it in your project.", actions: [{ label: "Finish", action: "dismiss" }], }, ] const service = useMachine(tour.machine, { id: useId(), steps }) const api = computed(() => tour.connect(service, normalizeProps)) const open = computed(() => api.value.open && api.value.step) </script> <template> <div> <button @click="api.start()">Start Tour</button> <div id="step-1">Step 1</div> </div> <Teleport to="body" v-if="open"> <div v-if="api.step?.backdrop" v-bind="api.getBackdropProps()" /> <div v-bind="api.getSpotlightProps()" /> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-if="api.step?.arrow" 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> <div v-if="api.step?.actions" class="tour button__group"> <button v-for="action in api.step?.actions" :key="action.label" v-bind="api.getActionTriggerProps({ action })" > {{ action.label }} </button> </div> <button v-bind="api.getCloseTriggerProps()">X</button> </div> </div> </Teleport> </template>
<script lang="ts"> import * as tour from "@zag-js/tour" import { portal, useMachine, normalizeProps } from "@zag-js/svelte" const id = $props.id() const service = useMachine(tour.machine, { id, steps }) const api = $derived(tour.connect(service, normalizeProps)) </script> <div> <div> <button onclick={() => api.start()}>Start Tour</button> <div id="step-1">Step 1</div> </div> {#if api.step && api.open} <div use:portal> {#if api.step.backdrop} <div {...api.getBackdropProps()}></div> {/if} <div {...api.getSpotlightProps()}></div> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {#if api.step.arrow} <div {...api.getArrowProps()}> <div {...api.getArrowTipProps()}></div> </div> {/if} <p {...api.getTitleProps()}>{api.step.title}</p> <div {...api.getDescriptionProps()}>{api.step.description}</div> <div {...api.getProgressTextProps()}>{api.getProgressText()}</div> {#if api.step.actions} <div> {#each api.step.actions as action} <button {...api.getActionTriggerProps({ action })}> {action.label} </button> {/each} </div> {/if} <button {...api.getCloseTriggerProps()}>X</button> </div> </div> </div> {/if} </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 doesn't have atargetdefined. -
"floating": Presents the step content as a floating element, which can be positioned flexibly on the screen. This usually doesn't have atargetdefined. -
"wait": A special type that waits for a specific condition before proceeding to the next step.
const steps: tour.StepDetails[] = [ // Tooltip step { id: "step-1", type: "tooltip", placement: "top-start", target: () => document.querySelector("#target-1"), title: "Tooltip Step", description: "This is a tooltip step", }, // Dialog step { id: "step-2", type: "dialog", title: "Dialog Step", description: "This is a dialog step", }, // Floating step { id: "step-3", type: "floating", placement: "top-start", title: "Floating Step", description: "This is a floating step", }, // Wait step { 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.goto(id): Jump to a specific step by id.dismiss(): Dismiss the tour immediately.show(): Call this method to show the current step.update(details: Partial<StepBaseDetails>): Call this method to update the current step details (for example, after data is 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.
Note: 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) }, }, ]
Showing progress dots
Use the api.getProgressPercent() to show the progress dots.
const ProgressBar = () => { const service = useMachine(tour.machine, { steps: [] }) const api = tour.connect(service, normalizeProps) return <div>{api.getProgressPercent()}</div> }
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.onStepsChange: Fires when the steps array is updated.onStatusChange: Fires when the status of the tour changes.
const Lifecycle = () => { const service = useMachine(tour.machine, { steps: [], onStepChange(details) { // => { stepId: "step-1", stepIndex: 0, totalSteps: 3, complete: false, progress: 0 } console.log(details) }, onStepsChange(details) { // => { steps: StepDetails[] } console.log(details.steps) }, onStatusChange(details) { // => { status: "started" | "skipped" | "completed" | "dismissed" | "not-found" } console.log(details.status) }, }) const api = tour.connect(service, normalizeProps) // ... }
Controlled current step
Use stepId and onStepChange for controlled navigation.
const service = useMachine(tour.machine, { steps, stepId, onStepChange(details) { setStepId(details.stepId) }, })
Programmatic tour control
Use the connected API to drive tour flow from code.
api.start() api.setStep("step-2") api.next() api.prev() api.updateStep("step-2", { title: "Updated title" })
Dismiss and interaction behavior
Configure dismissal and page interaction behavior with machine props.
const service = useMachine(tour.machine, { closeOnEscape: false, closeOnInteractOutside: false, preventInteraction: true, })
Customizing progress text
Use translations.progressText to customize the progress message.
const service = useMachine(tour.machine, { translations: { progressText: ({ current, total }) => `Step ${current} of ${total}`, }, })
Styling guide
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 can render tooltip, dialog, or floating content. You
can apply specific styles based on the rendered 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="content"][data-type="tooltip"] { /* styles for content when step is tooltip 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:
idsPartial<{ content: string; title: string; description: string; positioner: string; backdrop: string; arrow: string; }> | undefinedThe ids of the elements in the tour. Useful for composition.stepsStepDetails[] | undefinedThe steps of the tourstepIdstring | null | undefinedThe id of the currently highlighted steponStepChange((details: StepChangeDetails) => void) | undefinedCallback when the highlighted step changesonStepsChange((details: StepsChangeDetails) => void) | undefinedCallback when the steps changeonStatusChange((details: StatusChangeDetails) => void) | undefinedCallback when the tour is opened or closedcloseOnInteractOutsideboolean | undefinedWhether to close the tour when the user clicks outside the tourcloseOnEscapeboolean | undefinedWhether to close the tour when the user presses the escape keykeyboardNavigationboolean | undefinedWhether to allow keyboard navigation (right/left arrow keys to navigate between steps)preventInteractionboolean | undefinedPrevents interaction with the rest of the page while the tour is openspotlightOffsetanyThe offsets to apply to the spotlightspotlightRadiusnumber | undefinedThe radius of the spotlight clip pathtranslationsIntlTranslations | undefinedThe translations for the tourdir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside((event: PointerDownOutsideEvent) => void) | undefinedFunction called when the pointer is pressed down outside the componentonFocusOutside((event: FocusOutsideEvent) => void) | undefinedFunction called when the focus is moved outside the componentonInteractOutside((event: InteractOutsideEvent) => void) | undefinedFunction called when an interaction happens outside the component
Machine API
The tour api exposes the following methods:
openbooleanWhether the tour is opentotalStepsnumberThe total number of stepsstepIndexnumberThe index of the current stepstepStepDetails | nullThe current step detailshasNextStepbooleanWhether there is a next stephasPrevStepbooleanWhether there is a previous stepfirstStepbooleanWhether the current step is the first steplastStepbooleanWhether the current step is the last stepaddStep(step: StepDetails) => voidAdd a new step to the tourremoveStep(id: string) => voidRemove a step from the tourupdateStep(id: string, stepOverrides: Partial<StepDetails>) => voidUpdate a step in the tour with partial detailssetSteps(steps: StepDetails[]) => voidSet the steps of the toursetStep(id: string) => voidSet the current step of the tourstart(id?: string | undefined) => voidStart the tour at a specific step (or the first step if not provided)isValidStep(id: string) => booleanCheck if a step is validisCurrentStep(id: string) => booleanCheck if a step is visiblenextVoidFunctionMove to the next stepprevVoidFunctionMove to the previous stepgetProgressText() => stringReturns the progress textgetProgressPercent() => numberReturns the progress percent