Skip to content

Pickers and overlays

Tour

A guided sequence of modal dialogs anchored to existing controls.

When to use it: Use it for short, optional introductions to unfamiliar product areas; use inline help for instructions people need repeatedly.

On this page

Example

Loading example…
Tour.tsxtsx
import { Fragment } from "react";
import { Button, PopoverArrow, Tour, TourOverlay, TourTrigger } from "@comp0/react";

const projectTour = [
  {
    target: "project-search",
    title: "Find anything",
    description: "Search projects, people, and recent activity from one place.",
    placement: "bottom" as const,
  },
  {
    target: "project-notifications",
    title: "Review updates",
    description: "Notifications collect mentions and changes that need your attention.",
    placement: "bottom" as const,
  },
  {
    target: "new-project",
    title: "Create a project",
    description: "Start with a blank project or one of your team templates.",
    placement: "bottom" as const,
  },
];

export function Example() {
  return (
    <Tour steps={projectTour}>
      <div className="flex w-full max-w-2xl flex-col gap-5">
        <div className="flex items-center justify-between gap-4">
          <div>
            <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
              Project space
            </h3>
            <p className="text-xs text-zinc-500 dark:text-zinc-400">
              The tour content stays separate from these controls.
            </p>
          </div>
          <TourTrigger as={Fragment}>
            <Button className="select-none rounded bg-teal-700 px-3 py-2 text-sm font-medium text-white outline-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-teal-400 dark:text-zinc-950 dark:outline-teal-400">
              Start tour
            </Button>
          </TourTrigger>
        </div>
        <div className="grid grid-cols-3 gap-2 rounded-xl border border-zinc-950/10 bg-zinc-50 p-3 dark:border-white/10 dark:bg-zinc-900/60">
          <Button
            data-tour-target="project-search"
            className="relative select-none rounded-lg border border-zinc-950/10 bg-white px-3 py-5 text-sm font-medium text-zinc-700 outline-teal-600 hover:border-teal-500/50 focus-visible:outline-2 data-tour-active:z-10 data-tour-active:border-teal-500 data-tour-active:text-teal-800 data-tour-active:shadow-[0_0_0_4px_var(--color-teal-500),0_0_0_9999px_rgb(0_0_0/0.55)] dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-200 dark:outline-teal-400 dark:data-tour-active:text-teal-200"
          >
            Search
          </Button>
          <Button
            data-tour-target="project-notifications"
            className="relative select-none rounded-lg border border-zinc-950/10 bg-white px-3 py-5 text-sm font-medium text-zinc-700 outline-teal-600 hover:border-teal-500/50 focus-visible:outline-2 data-tour-active:z-10 data-tour-active:border-teal-500 data-tour-active:text-teal-800 data-tour-active:shadow-[0_0_0_4px_var(--color-teal-500),0_0_0_9999px_rgb(0_0_0/0.55)] dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-200 dark:outline-teal-400 dark:data-tour-active:text-teal-200"
          >
            Notifications
          </Button>
          <Button
            data-tour-target="new-project"
            className="relative select-none rounded-lg border border-zinc-950/10 bg-white px-3 py-5 text-sm font-medium text-zinc-700 outline-teal-600 hover:border-teal-500/50 focus-visible:outline-2 data-tour-active:z-10 data-tour-active:border-teal-500 data-tour-active:text-teal-800 data-tour-active:shadow-[0_0_0_4px_var(--color-teal-500),0_0_0_9999px_rgb(0_0_0/0.55)] dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-200 dark:outline-teal-400 dark:data-tour-active:text-teal-200"
          >
            New project
          </Button>
        </div>
        <TourOverlay
          offset={12}
          className="z-20 w-72 rounded-lg border-0 bg-white p-4 text-sm shadow-xl ring-1 ring-zinc-950/10 backdrop:bg-transparent dark:bg-zinc-900 dark:ring-white/10"
        >
          {({ step, stepIndex, stepCount, first, last, previous, next, close }) => (
            <>
              <PopoverArrow className="absolute -top-1 left-1/2 size-2 -translate-x-1/2 rotate-45 bg-white dark:bg-zinc-900" />
              <p className="mb-1 text-xs font-medium text-teal-700 dark:text-teal-300">
                Step {stepIndex + 1} of {stepCount}
              </p>
              <h4 className="font-semibold text-zinc-900 dark:text-zinc-100">{step.title}</h4>
              <p className="mt-1 text-zinc-600 dark:text-zinc-400">{step.description}</p>
              <div className="mt-4 flex items-center justify-between gap-3">
                <Button
                  onClick={close}
                  className="select-none rounded px-2 py-1.5 text-zinc-600 outline-teal-600 hover:bg-zinc-950/5 focus-visible:outline-2 dark:text-zinc-300 dark:outline-teal-400 dark:hover:bg-white/5"
                >
                  Skip tour
                </Button>
                <div className="flex gap-2">
                  {!first && (
                    <Button
                      onClick={previous}
                      className="select-none rounded border border-zinc-950/10 px-2.5 py-1.5 text-zinc-700 outline-teal-600 hover:bg-zinc-950/5 focus-visible:outline-2 dark:border-white/10 dark:text-zinc-200 dark:outline-teal-400 dark:hover:bg-white/5"
                    >
                      Back
                    </Button>
                  )}
                  <Button
                    onClick={next}
                    className="select-none rounded bg-teal-700 px-2.5 py-1.5 font-medium text-white outline-teal-600 focus-visible:outline-2 dark:bg-teal-400 dark:text-zinc-950 dark:outline-teal-400"
                  >
                    {last ? "Finish" : "Next"}
                  </Button>
                </div>
              </div>
            </>
          )}
        </TourOverlay>
      </div>
    </Tour>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Tour

    Wrapper-free owner for the current step, external target anchor, and focus restoration. Does not add a DOM element.

  2. TourTrigger

    Button that starts the tour at its first step. Owns a DOM element.

  3. TourOverlay

    Modal dialog anchored to the current external target. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Declare the ordered steps once with stable target names, titles, descriptions, and placements.

  2. 2

    Add the supporting parts

    Mark existing controls with matching data-tour-target attributes and add TourTrigger wherever the tour starts.

  3. 3

    Make the behavior clear

    Render the current step through TourOverlay; its state supplies progress, navigation, dismissal, target anchoring, and final focus restoration.

    Exampletsx
    <Tour steps={steps}>
      <TourTrigger>Start tour</TourTrigger>
      <Button data-tour-target="search">Search</Button>
      <TourOverlay aria-label="Product tour">
        {({ step, next }) => <Button onClick={next}>{step.title}</Button>}
      </TourOverlay>
    </Tour>;

Keyboard

↵
Starts the tour from TourTrigger.
Space
Starts the tour from TourTrigger.
⇥
Cycles through controls in the step dialog.
Esc
Closes the tour and restores TourTrigger focus.

Forms and accessibility

No native form behavior; controls targeted by the tour retain their existing behavior.

Accessibility checklist

  • Keep tours optional, short, and dismissible; do not hide required instructions exclusively inside a tour.
  • Give every application target one unique, stable data-tour-target value that matches its step definition.
  • Give TourOverlay an accessible name that describes the whole tour, while each step keeps a visible title.
  • Tour moves focus into the active dialog and restores the TourTrigger when the sequence closes.

API reference

Importtsx
import { Tour, TourTrigger, TourOverlay } from "@comp0/react";

Tour

Context only

Wrapper-free owner for the current step, external target anchor, and focus restoration.

PropTypeDescription
stepsreadonly TourStep[]Ordered target, title, description, and placement definitions; target names must be unique.
stepnumber | nullControlled active step index; null closes the tour.
defaultStepnumber | nullInitial uncontrolled step index; null keeps the tour closed.
onStepChange(step: number | null) => voidReceives each step change and null when the tour closes.

TourTrigger

DOM element

Button that starts the tour at its first step.

PropTypeDescription
asElementType | FragmentFragment merges the trigger behavior onto your own element child.

TourOverlay

DOM element

Modal dialog anchored to the current external target.

PropTypeDescription
aria-labelstringAccessible name for the guided sequence.
offsetnumberPixel gap between the active target and the dialog.
childrenReactNode | (state: TourState) => ReactNodeStatic content or a render function receiving the step, position, and navigation actions.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

Tour

Style hookMeaning
[data-tour-active]Applied to the external data-tour-target element for spotlight styling.

TourTrigger

Style hookMeaning
[data-open]The tour is open.

TourOverlay

Style hookMeaning
[data-open]The step dialog is visible.
[data-step]The zero-based active step index.
[data-target]The active step's target name.
[data-first]The first step is active.
[data-last]The final step is active.

Keep exploring