An accessible, unstyled React coachmark primitive for product tours, built on top of Base UI Popover.
pnpm add coachmarkreact and react-dom ≥18 are required peer dependencies.
Each step points to an element ref and composes a positioner, popup, and any content or controls it needs.
"use client";
import { useRef } from "react";
import { Coachmark } from "coachmark";
export function ToolbarTour() {
const profileRef = useRef<HTMLButtonElement>(null);
const settingsRef = useRef<HTMLButtonElement>(null);
return (
<>
<button ref={profileRef}>Profile</button>
<button ref={settingsRef}>Settings</button>
<Coachmark.Root>
<Coachmark.Trigger>Start tour</Coachmark.Trigger>
<Coachmark.Backdrop />
<Coachmark.Step target={profileRef}>
<Coachmark.Positioner sideOffset={12}>
<Coachmark.Popup>
<Coachmark.Arrow />
<Coachmark.Stepper>
{({ stepIndex, stepCount }) =>
`${stepIndex + 1} of ${stepCount}`
}
</Coachmark.Stepper>
<Coachmark.Title>Your profile</Coachmark.Title>
<Coachmark.Description>
Update your name and avatar here.
</Coachmark.Description>
<Coachmark.Close>Skip</Coachmark.Close>
<Coachmark.Next>Next</Coachmark.Next>
</Coachmark.Popup>
</Coachmark.Positioner>
</Coachmark.Step>
<Coachmark.Step target={settingsRef}>
<Coachmark.Positioner sideOffset={12}>
<Coachmark.Popup>
<Coachmark.Title>Settings</Coachmark.Title>
<Coachmark.Description>
Choose how the application behaves.
</Coachmark.Description>
<Coachmark.Previous>Back</Coachmark.Previous>
<Coachmark.Next>Done</Coachmark.Next>
</Coachmark.Popup>
</Coachmark.Positioner>
</Coachmark.Step>
</Coachmark.Root>
</>
);
}Coachmark ships no stylesheet. Style each part with className, style, the Base UI render prop, or its stable data-slot attribute.
The compound parts are available on Coachmark and as individual named exports such as CoachmarkRoot, CoachmarkStep, and CoachmarkPopup.
Rendered parts accept their native HTML props, including className and style. Parts built on Base UI also accept the corresponding Base UI Popover props, including render for replacing the default element. The tables below focus on behavior specific to Coachmark and the attributes available for styling.
Controls the tour's open state and active step. It does not render a DOM element.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
— | A trigger, optional backdrop, and one or more steps. |
open |
boolean |
— | Controls whether the tour is open. |
defaultOpen |
boolean |
false |
Sets the initial open state when uncontrolled. |
onOpenChange |
(open, details) => void |
— | Called when the open state changes. |
stepIndex |
number |
— | Controls the active step. |
defaultStepIndex |
number |
0 |
Sets the initial step when uncontrolled. |
onStepChange |
(index) => void |
— | Called when the active step changes. |
modal |
boolean | "trap-focus" |
true |
Configures Base UI's modal and focus-trapping behavior. |
resetOnOpen |
boolean |
true |
Resets the tour to its first step whenever it opens. |
scrollIntoView |
boolean | ScrollIntoViewOptions |
true |
Configures automatic scrolling for offscreen targets. |
Opens the tour. Renders a <button> by default and accepts all Base UI Popover Trigger props.
| Prop or attribute | Type or value | Description |
|---|---|---|
nativeButton |
boolean |
Whether a custom rendered element is a native button. |
disabled |
boolean |
Disables the trigger. |
openOnHover |
boolean |
Opens the tour when the trigger is hovered. |
delay / closeDelay |
number |
Configures hover open and close delays in milliseconds. |
render |
ReactElement | function |
Replaces the default button. |
data-slot |
"coachmark-trigger" |
Stable part selector. |
data-popup-open |
Present when open | Added by Base UI. |
data-pressed |
Present when pressed | Added by Base UI for press-opened popovers. |
Adds an optional backdrop and spotlight cutout. Renders a <div> in a portal and accepts all Base UI Popover Backdrop props except children.
| Prop or attribute | Type or value | Description |
|---|---|---|
render |
ReactElement | function |
Replaces the default element. |
data-slot |
"coachmark-backdrop" |
Stable part selector. |
data-open / data-closed |
Presence | Current open state from Base UI. |
data-starting-style / data-ending-style |
Presence | Base UI transition state. |
data-motion-state |
"entering" | "exiting" | "repositioning" | "idle" |
Current tour motion state. |
data-phase |
"closed" | "preparing-open" | "entering" | "exiting" | "scrolling" | "positioning" | "idle" |
Detailed presentation phase. |
data-transitioning |
Presence | Present while a step transition is active. |
data-concealed |
Presence | Present while an offscreen target is being prepared. |
The backdrop also exposes --coachmark-cutout-open and --coachmark-cutout-closed. Set --coachmark-cutout-path to override the path used by clip-path.
Defines one target and its content. It does not render a DOM element. Steps must be direct children of the root; fragments are supported, and their order defines the tour order.
| Prop | Type | Default | Description |
|---|---|---|---|
target |
RefObject<HTMLElement | null> |
— | Element highlighted and used as the positioner anchor. |
children |
ReactNode |
— | The positioner and popup for this step. |
spotlightPadding |
number |
8 |
Space around the spotlight cutout in CSS pixels. |
spotlightRadius |
number |
0 |
Corner radius of the spotlight cutout in CSS pixels. |
scrollIntoView |
boolean | ScrollIntoViewOptions |
Inherited | Overrides automatic scrolling for this step. |
Positions a step relative to its target. Renders a <div> in the step's portal and accepts Base UI Popover Positioner props except anchor, which comes from Step.target.
| Prop or attribute | Type or value | Default | Description |
|---|---|---|---|
side |
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" |
"bottom" |
Preferred side of the target. |
align |
"start" | "center" | "end" |
"center" |
Alignment along the chosen side. |
sideOffset |
number | function |
0 |
Distance from the target. |
alignOffset |
number | function |
0 |
Offset along the alignment axis. |
positionMethod |
"absolute" | "fixed" |
"absolute" |
CSS positioning strategy. |
collisionBoundary |
Element, elements, rect, or "clipping-ancestors" |
"clipping-ancestors" |
Boundary used for collision detection. |
collisionPadding |
number | Padding |
5 |
Space kept from the collision boundary. |
collisionAvoidance |
CollisionAvoidance |
Base UI default | Controls flipping and shifting when space is limited. |
sticky |
boolean |
false |
Keeps the popup visible after its target leaves the viewport. |
arrowPadding |
number |
5 |
Minimum space between the arrow and popup edges. |
disableAnchorTracking |
boolean |
false |
Stops tracking layout shifts of the target. |
data-slot |
"coachmark-positioner" |
— | Stable part selector. |
data-open / data-closed |
Presence | — | Current open state from Base UI. |
data-side |
Positioner side | — | Resolved side after collision handling. |
data-align |
Positioner alignment | — | Resolved alignment after collision handling. |
data-anchor-hidden |
Presence | — | Present when the target is hidden. |
data-instant |
String | — | Reason Base UI disabled its positioning transition. |
data-motion-state |
Motion state | — | Current tour motion state. |
data-phase |
Presentation phase | — | Detailed tour presentation phase. |
data-transitioning |
Presence | — | Present while a step transition is active. |
data-concealed |
Presence | — | Present while an offscreen target is being prepared. |
Base UI exposes positioning variables including --available-width, --available-height, --anchor-width, --anchor-height, and --transform-origin.
Contains the accessible step content. Renders a <div role="dialog"> and accepts all Base UI Popover Popup props.
| Prop or attribute | Type or value | Description |
|---|---|---|
initialFocus |
boolean | RefObject | function |
Chooses what receives focus when the tour opens. |
finalFocus |
boolean | RefObject | function |
Chooses what receives focus when the tour closes. |
render |
ReactElement | function |
Replaces the default element. |
data-slot |
"coachmark-popup" |
Stable part selector. |
data-open / data-closed |
Presence | Current open state from Base UI. |
data-starting-style / data-ending-style |
Presence | Base UI transition state. |
data-side / data-align |
String | Resolved placement from Base UI. |
data-instant |
String | Reason Base UI disabled its transition. |
data-motion-state |
Motion state | Current tour motion state. |
data-phase |
Presentation phase | Detailed tour presentation phase. |
data-transitioning |
Presence | Present while a step transition is active. |
data-concealed |
Presence | Present while an offscreen target is being prepared. |
When Viewport is used, the popup also receives --popup-width and --popup-height measurements.
Displays an optional arrow pointing toward the target. Renders a <div> and accepts all Base UI Popover Arrow props.
| Prop or attribute | Type or value | Description |
|---|---|---|
render |
ReactElement | function |
Replaces the default element. |
data-slot |
"coachmark-arrow" |
Stable part selector. |
data-open / data-closed |
Presence | Current open state from Base UI. |
data-side / data-align |
String | Resolved placement from Base UI. |
data-uncentered |
Presence | Present when the arrow cannot be centered on the target. |
Renders progress for the active step. Renders a <div> by default.
| Prop or attribute | Type or value | Description |
|---|---|---|
children |
ReactNode | (state) => ReactNode |
Static content or content derived from the tour state. |
render |
ReactElement | function |
Replaces the default element. |
data-slot |
"coachmark-stepper" |
Stable part selector. |
data-first-step / data-last-step |
Presence | Present for the corresponding boundary step. |
data-step-index / data-step-count |
Number | Current zero-based index and total count. Both are always present. |
data-motion-state |
Motion state | Current tour motion state. |
data-transitioning |
Presence | Present while a step transition is active. |
Provides the popup's accessible name. Renders an <h2> by default and accepts all Base UI Popover Title props.
| Prop or attribute | Type or value | Description |
|---|---|---|
render |
ReactElement | function |
Replaces the default heading. |
data-slot |
"coachmark-title" |
Stable part selector. |
Provides the popup's accessible description. Renders a <p> by default and accepts all Base UI Popover Description props.
| Prop or attribute | Type or value | Description |
|---|---|---|
render |
ReactElement | function |
Replaces the default paragraph. |
data-slot |
"coachmark-description" |
Stable part selector. |
Moves to the previous step. Renders a <button> by default and is not mounted on the first step unless keepMounted is enabled.
| Prop or attribute | Type or value | Default | Description |
|---|---|---|---|
children |
ReactNode | (state) => ReactNode |
— | Static content or content derived from the tour state. |
keepMounted |
boolean |
false |
Keeps the button mounted and disabled on the first step. |
render |
ReactElement | function |
— | Replaces the default button. |
data-slot |
"coachmark-previous" |
— | Stable part selector. |
data-first-step / data-last-step |
Presence | — | Present for the corresponding boundary step. |
data-step-index / data-step-count |
Number | — | Current zero-based index and total count. Both are always present. |
data-motion-state |
Motion state | — | Current tour motion state. |
data-transitioning |
Presence | — | Present while a step transition is active. |
Moves forward, or finishes and closes the tour on the final step. Renders a <button> by default.
| Prop or attribute | Type or value | Description |
|---|---|---|
children |
ReactNode | (state) => ReactNode |
Static content or content derived from the tour state. |
render |
ReactElement | function |
Replaces the default button. |
data-slot |
"coachmark-next" | "coachmark-finish" |
Changes to coachmark-finish on the final step. |
data-first-step / data-last-step |
Presence | Present for the corresponding boundary step. |
data-step-index / data-step-count |
Number | Current zero-based index and total count. Both are always present. |
data-motion-state |
Motion state | Current tour motion state. |
data-transitioning |
Presence | Present while a step transition is active. |
Function children receive the complete tour state, so labels can change on the final step:
<Coachmark.Next render={<Button variant="primary" />}>
{({ isLastStep }) => (isLastStep ? "Finish" : "Continue")}
</Coachmark.Next>Closes the tour early. Renders a <button> and accepts all Base UI Popover Close props.
| Prop or attribute | Type or value | Description |
|---|---|---|
nativeButton |
boolean |
Whether a custom rendered element is a native button. |
render |
ReactElement | function |
Replaces the default button. |
data-slot |
"coachmark-close" |
Stable part selector. |
Optionally snapshots the previous step's content so size and content changes can be animated. Renders a <div> by default.
| Prop or attribute | Type or value | Description |
|---|---|---|
children |
ReactNode |
Content that changes between steps. Keep the arrow outside the viewport. |
render |
ReactElement | function |
Replaces the default element. |
data-slot |
"coachmark-viewport" |
Stable part selector. |
data-activation-direction |
Space-separated directions | Direction from the previous target, such as "right down". |
data-first-step / data-last-step |
Presence | Present for the corresponding boundary step. |
data-step-index / data-step-count |
Number | Current zero-based index and total count. Both are always present. |
data-motion-state |
Motion state | Current tour motion state. |
data-transitioning |
Presence | Present while previous and current content are rendered together. |
data-current / data-previous |
Presence on child wrappers | Identifies incoming and outgoing content. |
data-starting-style / data-ending-style |
Presence on child wrappers | Marks the active content transition phase. |
When a viewport transition is active, the popup receives --popup-width and --popup-height; the positioner receives --positioner-width and --positioner-height.
Returns the current tour state and navigation actions. It must be used inside Coachmark.Root.
| Value | Type | Description |
|---|---|---|
stepIndex / stepCount |
number |
Current zero-based index and total number of steps. |
isFirstStep / isLastStep |
boolean |
Whether the active step is at either boundary. |
motionState |
"entering" | "exiting" | "repositioning" | "idle" |
Current motion state. |
transitioning |
boolean |
Whether a step transition is active. |
next() / previous() |
() => void |
Moves one step in either direction. |
goTo(index) |
(number) => void |
Moves to a specific step. |
close() |
() => void |
Closes the tour without changing the active step. |
finish() |
() => void |
Moves to the final step and closes the tour. |
pnpm install
pnpm devpnpm dev watches the library and runs the playground. Use pnpm check to run type checking and tests.
MIT