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