Coachmark

PreviousNext

An onboarding tour — a scrim with a cutout on the target, an attention ring, and an anchored card. Three things moving at once, on three deliberately different tiers.

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/coachmark.json

Usage

import { Coachmark, type CoachmarkStep } from '@/components/matos-ui/coachmark'
const steps: CoachmarkStep[] = [
  { target: '#new-button', title: 'Start here', description: 'Create anything from one place.' },
  { target: '#search', title: 'Jump anywhere', description: 'Also ⌘K from any screen.' },
]

const [open, setOpen] = useState(false)

<button onClick={() => setOpen(true)}>Take the tour</button>
<Coachmark steps={steps} open={open} onOpenChange={setOpen} />

A target is a CSS selector, an Element, a ref, or a function returning one.

Three tiers, on purpose

Three things move the moment a step opens, and none of them can share timing:

ElementTierWhy
Scrim (cutout)spring.gentle (0.6s, ~no bounce)Ambient — it should recede, not announce. The cutout glides between steps on the same tier.
Cardspring.moderateA panel that has to land exactly where it was aimed.
RingattentionGlow, once per stepA "look here" cue — no offset, no elevation, nothing to compose with. Not a tier.

spring.gentle on the card would drift; spring.moderate on the scrim would snap; a looping glow would nag. This component is the one place all three are on screen together, and the contrast is the argument.

How it's built

  • The scrim is a box-shadow. A cutout element the size of the target casts a 100vmax shadow — that shadow is the dimming. Moving it (x / y / width / height) on gentle glides the spotlight; a flat click-catcher behind it blocks the page.
  • The card is Elevated offset={4}, portaled to document.body with the substrate reset to the page (like Achievement Toast), so that rung means the same wherever the tour runs. It flips above the target when there's no room below.
  • The ring fires attentionGlow once when rect first resolves for a step — never on the re-measure poll that follows the target into view.
  • prefers-reduced-motion drops the glide, the card slide and the glow; scrolling to the target is instant, and the scrim and card still fade.

Reference

Coachmark Props

PropTypeDefaultDescription
stepsCoachmarkStep[]The tour, in order.
openbooleanControlled visibility.
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void
stepnumberControlled step index.
defaultStepnumber0
onStepChange(step: number) => void
onComplete() => voidFires when the last step is passed.
dismissiblebooleantrueScrim click / Esc closes; shows a Skip / ×.
labelsPartial<{ back, next, done, skip, of }>Button and counter text.

CoachmarkStep

FieldTypeDescription
targetstring | Element | Ref | () => ElementWhat to spotlight.
titleReactNode
descriptionReactNode
placement"top" | "bottom"Forces the card side.
paddingnumberCutout breathing room (default 8).