Tour
A tour of callouts: each step an ink callout hung on a leader line that ends in a dot on the thing it is about, one step at a time, only when you press on.
Report an issue with TourExample
A step
Send the draft
Send it
03/04Nothing leaves until you press this.
Next, focus
Send the draft
Send it
03/04Nothing leaves until you press this.
The last step
Send the draft
Send it
04/04Nothing leaves until you press this.
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and dialog along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/tour.jsonUsage
This example also uses Button, Field and Meta.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ button.json https:// thedirectors.agency/ ui/ r/ field.json https:// thedirectors.agency/ ui/ r/ meta.json"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Field, Input } from "@/components/ui/field"import { Meta } from "@/components/ui/meta"import { Tour, TourContent, TourStep, TourTrigger } from "@/components/ui/tour"export default function Example() {const title = React.useRef<HTMLDivElement>(null)const send = React.useRef<HTMLButtonElement>(null)return (<div className="grid w-full max-w-xl gap-y-10"><Meta id="tour-draft-meta"><span>Draft</span><span>Saved a minute ago</span></Meta><div ref={title}><Field label="Working title"><Input defaultValue="Notes on the crossing" autoComplete="off" /></Field></div><div className="flex flex-wrap items-center justify-between gap-6"><Tour><TourTrigger asChild><Button variant="quiet">Show me round</Button></TourTrigger><TourContent><TourStep title="A draft of your own">Four things to know before you send it. Next, or the arrow keys, to go on; Escape to stop.</TourStep><TourStep target="#tour-draft-meta" title="Where it stands">The frame row says what this is and when it was last kept. It saves as you write.</TourStep><TourStep target={title} title="Its name">Say what it is about. You can change it until the day it goes out.</TourStep><TourStep target={send} title="Send it">Nothing leaves until you press this, and you can take it back for an hour after.</TourStep></TourContent></Tour><Button ref={send} variant="bracket">Send the draft</Button></div></div>)}/** A step drawn still, in the page: a word, the leader and the callout. */function Still({ at, of, last, force }: { at: number; of: number; last?: boolean; force?: string }) {return (<div style={{ position: "relative", width: "min(20rem, calc(100vw - 5.2rem))", height: "13rem" }}><span style={{ position: "absolute", left: 0, top: 0, color: "var(--ot-ink)" }}>Send the draft</span><svg className="ot-tour-lead" aria-hidden="true"><line className="ot-tour-line" x1="56" y1="26" x2="130" y2="84" pathLength={1} /><circle className="ot-tour-dot" cx="56" cy="26" r={3.5} /></svg><div className="ot-tour-callout" data-laid="" style={{ left: "min(112px, calc(100vw - 5.2rem - 13rem))", top: 84, animation: "none", maxInlineSize: "13rem" }}><div className="ot-tour-head"><p className="ot-tour-title">Send it</p><span className="ot-tour-count">0{at}/0{of}</span></div><p className="ot-tour-text">Nothing leaves until you press this.</p><div className="ot-tour-actions"><button type="button" className="ot-tour-end">End</button><button type="button" data-go="back">Back</button><button type="button" data-go="next" data-last={last ? "" : undefined} data-force={force}>{last ? "Done" : "Next"}</button></div></div></div>)}Contract
- Underneath: native
<dialog>withshowModal(), through the dialog item. - Anatomy:
<dialog class="ot-tour">the size of the window and see-through (no backdrop), holding<svg class="ot-tour-lead" aria-hidden="true">(aline.ot-tour-lineand acircle.ot-tour-dot) and.ot-tour-callout:.ot-tour-head(theh2.ot-tour-titleand.ot-tour-count, "03/04" with "Step 3 of 4" for screen readers),.ot-tour-text, then.ot-tour-actions(End, Back, Next or Done). - After the Weingart letter: each step is an ink callout, its head in bold and the count at its far end as a corner note, hung on a long leader line that ends in a dot on the thing it is about. The dot is the accent (look here), set on the target's edge facing the callout; the leader leans off it toward the roomier side of the window and the callout hangs a space-7 beyond, held inside the window. A step with no target stands alone in the middle with no leader. Each step arrives as the note does, in turn: the dot lands (spiccato), the leader draws out, the callout spreads from the leader's tip (
clip-path: circle()). - It moves only when the person does: Next, Back, or the arrow keys (swapped right to left); a target out of view is scrolled to the middle first. It never plays by itself. The last step's Next reads "Done." and closes; End or Escape stops it anywhere. Uncontrolled, it starts again from the first step each time it opens.
- Keyboard and screen readers: focus opens on Next and stays on the control that moved it (Back, or Next once Back is gone). The title names the dialog and the text describes it; Next and Back are described by the count, the title and the text, so each new step is read as focus lands. On the ink the focus ring is in the paper's colour.
- Reduced motion: each step appears whole, and the scroll jumps. Forced colours: the callout keeps a border in CanvasText, the leader is CanvasText and the dot Highlight.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
Tour | open, defaultOpen, onOpenChange | The same as Dialog. Built on the native dialog, so focus, Escape and the top layer are the browser's. | |
Tour value / defaultValue / onValueChange | number, (value) => void | 1 | The step you're on, counted from 1. Uncontrolled, the tour starts again from the first step each time it opens. |
TourTrigger | button | Opens the tour. With asChild, your own button. | |
TourContent | dialog | The window-wide, see-through dialog. Its children are the TourSteps, in order. Next and Back (the arrow keys too, swapped right to left) move a step; Escape or End stops; the last step's Next reads Done and closes. Focus stays on the control that moved it. | |
TourContent labels | { back, next, done, end } | "Back", "Next", "Done", "End" | The words on the callout's controls. |
TourStep target | string | RefObject<Element> | What the step is about: a selector or a ref. Scrolled into view when it isn't. Left out, the callout stands alone in the middle of the window, with no leader: an opening or a closing word. | |
TourStep title | ReactNode | The step's name, bold at the head of the callout. It names the dialog. | |
TourStep children | ReactNode | A sentence or two, phrasing content. It describes the dialog, and Next and Back, so it is read when focus lands. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Tour | Weingart letter | Ink callouts hung on leader lines that end in a dot on the thing they are about |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Tour | Arpeggio | On Next or Back only: the dot lands, the leader draws out, the callout spreads from its tip |