Skip to content

Tour

Walk people through a screen one element at a time, with a spotlight and a card beside it.

import { Tour, TourBeacon, TourStep } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a Tour to show someone around a screen they haven't used before, or a few new things on one they have. Keep it short: three to five steps, each about one thing they can see. For one new control a Popover or a TourBeacon with a one-step tour is enough.

import { useState } from 'react'
import { Button, Tour, type TourStep } from '@youj/react'

const steps: TourStep[] = [
  { target: '#search', title: 'Find any order', body: 'Search by number, customer or amount.' },
  { target: '#filters', title: 'Narrow the list', body: 'Filter by status, channel or date.' },
  { target: '#export', title: 'Take it with you', body: 'Export what you see as CSV.', placement: 'top' },
]

export function OrdersTour() {
  const [open, setOpen] = useState(false)
  return (
    <>
      <Button onClick={() => setOpen(true)}>Take the tour</Button>
      <Tour steps={steps} open={open} onOpenChange={setOpen} onComplete={markSeen} onSkip={markSeen} />
    </>
  )
}

A step's target is a CSS selector (the first match on the page) or a ref. A step whose target isn't on the page, or is hidden, is skipped in the direction you were going. placement puts the card on a side of the target (bottom by default; top, inline-start or inline-end), and it flips when there's no room. action adds your own content under the text, such as a link or a button that does the thing.

open and step can each be controlled (onOpenChange, onStepChange), or left to the tour with defaultOpen and defaultStep; an uncontrolled tour starts again from defaultStep each time it opens. onComplete runs when Done is pressed, onSkip with the step index when Skip or Escape ends it early.

To keep the tour inside part of the page, as these previews do, pass that element as container and give it contain: paint (or a transform), so the scrim covers it rather than the window.

Reference

Propssteps, open, step, onComplete, onSkip, allowInteraction, dismissible, container…

Props

Tour

Tour props
PropType
stepsRequiredTourStep[]–
openboolean–
defaultOpenboolean

default false

false
onOpenChange(open: boolean) => void–
stepnumber

Controlled step index.

–
defaultStepnumber

default 0

0
onStepChange(step: number) => void–
onComplete() => void

Runs when Done is pressed on the last step (or Next runs past the last step that can be shown).

–
onSkip(step: number) => void

Runs when the tour is left early, by Skip or Escape, with the step it was on.

–
allowInteractionboolean

Clicks reach the spotlighted element, and focus is not trapped in the card, so the target can be used.

default false

false
dismissibleboolean

Skip and Escape end the tour. With false the only ways out are Done and your own controls.

default true

true
spotlightPaddingnumber

Room around the target inside the cut-out, in px.

default 6

6
containerHTMLElement | null

Where the scrim and card render. Default: the provider's portal container, else document.body. An element of your own keeps the tour inside it; it must be a positioning context for fixed children (give it a transform or contain: paint).

–
classNamestring

The card's classes.

–

TourBeacon

TourBeacon props
PropType
labelstring

The button's accessible name. Default: the tourStart label ("Start the tour").

–
Other props pass through: extends ComponentProps<'button'>.

TourBeacon also takes a button's props (onClick, className…).

Data attributes

The card is data-slot="tour", the scrim data-slot="tour-scrim" and the outline around the hole data-slot="tour-spotlight". In the card, tour-count, tour-title, tour-body and tour-dots mark the parts, and the current dot carries data-state="active". The beacon is data-slot="tour-beacon".

MotionGlide · card · open, close · beacon · reduced motion

Motion

Motion tokens
MomentTokens
Glidethe hole moves and resizes from the last target to the next, following it live while the page scrolls it into view
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Cardthe next step’s text fades in, rising 4px, where the card lands
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Open, closethe scrim fades (yj-backdrop) and the card opens with yj-pop
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Scrolla target off screen scrolls to the middle smoothly
no token

Beacona ring swells out of the dot and fades, repeating while it is on screen
duration.deliberateease.exit

560ms · cubic-bezier(0.4, 0, 1, 1)

560mscubic-bezier(0.4, 0, 1, 1)

Reduced motion

The hole moves to each target at once and scrolling is instant; only the card's text fades. The beacon's ring rests still around the dot.

Reduced motion

Every step lands at once, so nothing glides or slides; what changes is still shown, and announced.

AccessibilityFocus, keyboard, ARIA, live region

Accessibility

  • The card is a dialog named by the step's title and described by its text, followed by a hint on the keys. Focus moves into it (to Next) when the tour opens and stays in it while the tour runs.
  • ← and → go Back and Next (swapped in right-to-left), Tab moves between the buttons, and Escape skips the tour when it's dismissible.
  • Each new step is announced politely: "Step 2 of 4: Find any order".
  • The spotlighted element is described by the step's text (aria-describedby) while it's shown, and its own description is put back afterwards.
  • With allowInteraction, clicks reach the spotlighted element and focus isn't held in the card, so the element can be used from the keyboard too.
  • The count and the dots say the same thing; the dots are hidden from assistive technology.
  • TourBeacon is a button named "Start the tour" by default; pass label to say what it's about.