Skip to main content

A tour guides users through product features with step-by-step overlays.

Loading...

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

Anatomy

Check the tour anatomy and part names.

Each part includes a data-part attribute 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" }], }, ]

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 a target defined.

  • "floating": Presents the step content as a floating element, which can be positioned flexibly on the screen. This usually doesn'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 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, })

Exit animations (Presence)

After the tour closes, api.step still references the last step so exit animations can keep rendering content. Drive visibility with api.open (or Presence), and read content from api.step.

{ api.step && ( <Presence present={api.open} unmountOnExit> <div {...api.getContentProps()}> <p {...api.getTitleProps()}>{api.step.title}</p> </div> </Presence> ) }

Don't treat api.step === null as "tour closed". Use api.open or onStatusChange for that.

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 */ }

Z-Index

The machine sets --tour-layer on the layered parts (0 backdrop, 1 spotlight, 2 positioner). Apply it in CSS so content stacks above the backdrop:

:root { --tour-z-index: 100; } [data-scope="tour"][data-part="backdrop"], [data-scope="tour"][data-part="spotlight"], [data-scope="tour"][data-part="positioner"], [data-scope="tour"][data-part="content"] { z-index: calc(var(--tour-layer) + var(--tour-z-index)); }

Spotlight

The spotlight exposes CSS variables you can transition between steps:

[data-scope="tour"][data-part="spotlight"] { transition: left 0.2s ease, top 0.2s ease, width 0.2s ease, height 0.2s ease; }

Variables: --spotlight-x, --spotlight-y, --spotlight-width, --spotlight-height.

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:

  • idsElementIds | undefinedThe ids of the elements in the tour. Useful for composition.
  • stepsStepDetails[] | undefinedThe steps of the tour
  • stepIdstring | null | undefinedThe id of the currently highlighted step
  • onStepChange((details: StepChangeDetails) => void) | undefinedCallback when the highlighted step changes
  • onStepsChange((details: StepsChangeDetails) => void) | undefinedCallback when the steps change
  • onStatusChange((details: StatusChangeDetails) => void) | undefinedCallback when the tour is opened or closed
  • closeOnInteractOutsideboolean | undefinedWhether to close the tour when the user clicks outside the tour
  • closeOnEscapeboolean | undefinedWhether to close the tour when the user presses the escape key
  • keyboardNavigationboolean | 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 open
  • spotlightOffsetPoint | undefinedThe offsets to apply to the spotlight
  • spotlightRadiusnumber | undefinedThe radius of the spotlight clip path
  • translationsIntlTranslations | undefinedThe translations for the tour
  • dir"ltr" | "rtl" | undefinedThe document's text/writing direction.
  • idstringThe unique identifier of the machine.
  • getRootNode(() => ShadowRoot | Document | Node) | 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 component
  • onFocusOutside((event: FocusOutsideEvent) => void) | undefinedFunction called when the focus is moved outside the component
  • onInteractOutside((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 open
  • totalStepsnumberThe total number of steps
  • stepIndexnumberThe index of the current step
  • stepStepDetails | nullThe current step details
  • hasNextStepbooleanWhether there is a next step
  • hasPrevStepbooleanWhether there is a previous step
  • firstStepbooleanWhether the current step is the first step
  • lastStepbooleanWhether the current step is the last step
  • addStep(step: StepDetails) => voidAdd a new step to the tour
  • removeStep(id: string) => voidRemove a step from the tour
  • updateStep(id: string, stepOverrides: Partial<StepDetails>) => voidUpdate a step in the tour with partial details
  • setSteps(steps: StepDetails[]) => voidSet the steps of the tour
  • setStep(id: string) => voidSet the current step of the tour
  • start(id?: string) => voidStart the tour at a specific step (or the first step if not provided)
  • isValidStep(id: string) => booleanCheck if a step is valid
  • isCurrentStep(id: string) => booleanCheck if a step is visible
  • nextVoidFunctionMove to the next step
  • prevVoidFunctionMove to the previous step
  • getProgressText() => stringReturns the progress text
  • getProgressPercent() => numberReturns the progress percent
Edit this page on GitHub