<!-- Protocore Design System — Tour -->
# Tour

- **Category:** Feedback (`feedback`)
- **Slug:** `feedback/tour`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Tour } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/feedback/tour

> A controlled product-tour / coachmark sequence — spotlights each target element and annotates it with a stepped Popover card.

## When to use it

Reach for a **Tour** to orient a user *the first time* they land on a dense surface — a new console, a migrated screen, a feature behind a flag. Each step should teach one anchored affordance and then get out of the way.

- Explaining a *single* control in place? Use a [HelpTip](/feedback/help-tip) or [Popover](/overlay/popover) — a whole tour is too heavy.
- Showing linear *progress through a task* (not a walkthrough)? Use [Steps](/navigation/steps).
- Something that must block until acknowledged? Use a [Dialog](/overlay/dialog).

Keep tours short (three to five steps), let users Skip at any point, and never trap essential information inside one — assume it will be dismissed.

## Targeting

A step's `target` is either a `RefObject` to a mounted element or a CSS `selector` string resolved against the document when the step activates. The target is scrolled into view, measured, and re-measured on scroll/resize, so the spotlight and card track the element. If a target can't be found, the card centres itself with no cutout rather than pointing at nothing.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `backLabel` | `string` | no | `Back` | Label for the back button. Defaults to `"Back"`. |
| `className` | `string` | no | — | Extra class on the card. |
| `defaultIndex` | `number` | no | `0` | Initial active step index in uncontrolled mode. Defaults to `0`. |
| `defaultOpen` | `boolean` | no | `false` | Initial open state in uncontrolled mode. Defaults to `false`. |
| `doneLabel` | `string` | no | `Done` | Label for the advance button on the final step. Defaults to `"Done"`. |
| `index` | `number` | no | — | Controlled active step index (0-based). Pair with `onIndexChange`. |
| `nextLabel` | `string` | no | `Next` | Label for the advance button on non-final steps. Defaults to `"Next"`. |
| `onFinish` | `(() => void)` | no | — | Called when the user completes the final step ("Done"). |
| `onIndexChange` | `((index: number) => void)` | no | — | Called when the active step index changes. |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Called whenever the tour opens or closes. |
| `onSkip` | `(() => void)` | no | — | Called when the user dismisses the tour (Skip, Escape, or scrim click). |
| `open` | `boolean` | no | — | Controlled open state. Pair with `onOpenChange`. |
| `skipLabel` | `string` | no | `Skip` | Label for the skip control. Defaults to `"Skip"`. |
| `spotlightPadding` | `number` | no | `8` | Padding in px around the target inside the spotlight cutout. Defaults to `8`. |
| `steps` | `TourStep[]` | yes | — | Ordered steps to walk through. |

## Examples

### Basics

Give `Tour` a `steps` array — each step points at a `target` (a ref or CSS selector), a `title`, and a `body`. Opening it dims the page, punches a sharp spotlight over the current target, and floats a card beside it with a mono `[ 01 / 04 ]` index and Back / Next / Done + Skip controls.

```tsx
import { useRef, useState } from "react";
import { Tour, Button, Card, Stack, HStack, Input, Field } from "@protocore/pds";
import type { TourStep } from "@protocore/pds";

export default function TourBasics() {
  const clusterRef = useRef<HTMLDivElement>(null);
  const keyRef = useRef<HTMLButtonElement>(null);
  const deployRef = useRef<HTMLButtonElement>(null);
  const [open, setOpen] = useState(false);

  const steps: TourStep[] = [
    {
      target: clusterRef,
      title: "Pick a cluster",
      body: "Choose which environment this release ships to. Staging mirrors production topology.",
    },
    {
      target: keyRef,
      title: "Rotate the signing key",
      body: "Every deploy is signed. Rotate here if the key was exposed — old artifacts stay verifiable.",
    },
    {
      target: deployRef,
      title: "Ship it",
      body: "Deploy runs the migration, rebuilds, and restarts the node. You can roll back for 24 hours.",
    },
  ];

  return (
    <Stack gap={4}>
      <HStack gap={3}>
        <Button onClick={() => setOpen(true)}>Start tour</Button>
      </HStack>

      <Card title="Release" subtitle="protocore-cms · main">
        <Stack gap={4}>
          <Field label="Target cluster">
            <div ref={clusterRef}>
              <Input defaultValue="staging.protocore.io" readOnly />
            </div>
          </Field>
          <HStack gap={2}>
            <Button ref={keyRef} variant="secondary">
              Rotate key
            </Button>
            <Button ref={deployRef}>Deploy</Button>
          </HStack>
        </Stack>
      </Card>

      <Tour
        steps={steps}
        open={open}
        onOpenChange={setOpen}
        onFinish={() => setOpen(false)}
        onSkip={() => setOpen(false)}
      />
    </Stack>
  );
}
```

### Controlled + first-run gating

Drive `open` and `index` yourself to resume a half-finished tour, branch steps, or fire the tour once per user. `onFinish` runs when the last step's **Done** is pressed; `onSkip` runs on Skip, Escape, or a scrim click — persist whichever fired so you don't show it again.

```tsx
import { useRef, useState } from "react";
import { Tour, Button, Card, Stack, HStack, Badge, Tag } from "@protocore/pds";
import type { TourStep } from "@protocore/pds";

export default function TourControlled() {
  const statusRef = useRef<HTMLSpanElement>(null);
  const versionRef = useRef<HTMLSpanElement>(null);
  const [open, setOpen] = useState(false);
  const [index, setIndex] = useState(0);
  const [outcome, setOutcome] = useState<"finished" | "skipped" | null>(null);

  const steps: TourStep[] = [
    {
      target: statusRef,
      title: "Node health",
      body: "Green means the validator is signing blocks. Amber is degraded; red is offline.",
    },
    {
      target: versionRef,
      title: "Running version",
      body: "The agent version this node reports. A mismatch across the fleet flags a stalled rollout.",
    },
  ];

  return (
    <Stack gap={4}>
      <HStack gap={3}>
        <Button
          onClick={() => {
            setIndex(0);
            setOutcome(null);
            setOpen(true);
          }}
        >
          Explain this row
        </Button>
        {outcome ? <Tag>{outcome === "finished" ? "tour: completed" : "tour: skipped"}</Tag> : null}
      </HStack>

      <Card title="val-04" subtitle="eu-central-1b">
        <HStack gap={4} align="center">
          <Badge ref={statusRef} tone="success">
            Signing
          </Badge>
          <Tag ref={versionRef}>agent v2.9.1</Tag>
        </HStack>
      </Card>

      <Tour
        steps={steps}
        open={open}
        onOpenChange={setOpen}
        index={index}
        onIndexChange={setIndex}
        onFinish={() => setOutcome("finished")}
        onSkip={() => setOutcome("skipped")}
      />
    </Stack>
  );
}
```

## Do & don't

**Do**

- Keep tours to three to five anchored, single-idea steps.
- Always leave Skip reachable and honour Escape as skip.
- Persist onFinish/onSkip so a first-run tour shows once.
- Point each step at a stable element — a ref beats a brittle selector.

**Don't**

- Don't bury required information or the only copy of an action inside a tour.
- Don't chain a dozen steps — split long onboarding into contextual HelpTips.
- Don't target elements that may be unmounted when the step activates.
- Don't run a tour on every visit; gate it on first run.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab / Shift+Tab` | Move between the card's controls (focus is trapped in the card). |
| `Enter / →` | Advance to the next step, or finish on the last step. |
| `←` | Return to the previous step. |
| `Esc` | Skip and dismiss the tour. |

**Notes**

- The card is a modal dialog: focus moves into it on open and is trapped until the tour closes.
- Focus lands on the primary advance button each step so keyboard users can walk the whole tour with Enter.
- The spotlight scrim is decorative (aria-hidden); step content lives in the labelled dialog card.
- The mono step index is presentational — the card exposes its role and label to assistive tech independently.

## Related

`popover`, `steps`, `dialog`, `help-tip`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
