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
- Support for different step types such as "dialog", "floating", "tooltip" or "wait".
- Support for 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
To use the tooltip machine in your project, run the following command in your command line:
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
To set up the tooltip correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the tooltip package into your project
import * as tour from "@zag-js/tour"
The tour package exports two key functions:
machine— The state machine logic for the tour widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
Next, import the required hooks and functions for your framework and use the tour machine in your project 🔥
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 { useMachine, normalizeProps } 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 qrCode from "@zag-js/qr-code" import { portal, useMachine, normalizeProps } from "@zag-js/svelte" const id = $props.id() const service = useMachine(qrCode.machine, { id, steps }) const api = $derived(qrCode.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 don't have atargetdefined. -
"floating": Presents the step content as a floating element, which can be positioned flexibly on the screen. This usually don'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.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.
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.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) }, onStatusChange(status) { // => { status: "started" | "skipped" | "completed" | "dismissed" | "not-found" } console.log(status) }, }) const api = tour.connect(service, normalizeProps) // ... }
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 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:
idsPartial<{ content: string; title: string; description: string; positioner: string; backdrop: string; arrow: string; }>The ids of the elements in the tour. Useful for composition.stepsStepDetails[]The steps of the tourstepIdstringThe id of the currently highlighted steponStepChange(details: StepChangeDetails) => voidCallback when the highlighted step changesonStepsChange(details: StepsChangeDetails) => voidCallback when the steps changeonStatusChange(details: StatusChangeDetails) => voidCallback when the tour is opened or closedcloseOnInteractOutsidebooleanWhether to close the tour when the user clicks outside the tourcloseOnEscapebooleanWhether to close the tour when the user presses the escape keykeyboardNavigationbooleanWhether to allow keyboard navigation (right/left arrow keys to navigate between steps)preventInteractionbooleanPrevents interaction with the rest of the page while the tour is openspotlightOffsetPointThe offsets to apply to the spotlightspotlightRadiusnumberThe radius of the spotlight clip pathtranslationsIntlTranslationsThe translations for the tourdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the componentonFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the componentonInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
Machine API
The time picker api exposes the following methods:
openbooleanWhether the tour is opentotalStepsnumberThe total number of stepsstepIndexnumberThe index of the current stepstepStepDetailsThe 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) => 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
Edit this page on GitHub