Onboarding
A full-surface onboarding flow shell. It renders one phase at a time with a cross-fade between phases; the consumer owns the active phase via currentPhase and reacts to onAdvance. The shell stays unopinionated about progression rules, persistence, and analytics — those live where they belong (in the parent).
Different from Explainer: Explainer is a compound primitive for lesson/walkthrough phase walks where the shell owns navigation state. Onboarding is a top-of-app first-time-user flow where the parent typically owns the phase state because it persists "onboarded" status, fires analytics on each advance, and may branch between phase sequences per user segment.
Welcome
A three-phase onboarding flow. The shell cross-fades between phases; you own the state.
Installation
npx shadcn@latest add https://craftbits.dev/r/onboarding.jsonUsage
import { Onboarding, type OnboardingPhase } from "@craft-bits/core";
const PHASES: OnboardingPhase[] = [
{ id: "welcome", label: "Welcome", content: <Welcome /> },
{ id: "try", label: "Try it", content: (ctx) => <Try onDone={ctx.advance} /> },
{ id: "done", label: "All set", content: <Done /> },
];
function App() {
const [phase, setPhase] = useState("welcome");
return (
<Onboarding
phases={PHASES}
currentPhase={phase}
onAdvance={(next) => next && setPhase(next)}
/>
);
}Phase content can be a raw ReactNode or a render function receiving the phase context ({ currentPhase, index, total, isLast, advance }). Use the render-fn form when the body needs to wire a Continue button straight to ctx.advance without lifting state.
Understanding the component
- External state.
currentPhaseis the truth — the shell does not own it. The consumer reacts toonAdvance(nextPhaseId)to update its own state.nextPhaseIdisnullwhen the last phase tried to advance. - Phase array drives order. The order in
phasesIS the progression order.onAdvanceis fired withphases[index + 1].id. Reordering the array reorders the flow. - Cross-fade transitions. Each phase enters with a fade and exits with a fade plus a small upward translate.
AnimatePresenceruns inmode="wait"so the exiting phase clears before the next one mounts. Respectsprefers-reduced-motion— the transition collapses to a plain opacity swap. - Render-fn content.
contentmay beReactNodeor(ctx) => ReactNode. The context exposesadvance,isLast,index,total, and the resolvedcurrentPhaseso the body can wire its own progression UI without lifting state. - Fallback for unknown ids. When
currentPhasedoesn't match anyphases[].id, the shell renders the first phase as a safety net — common during initial load before the consumer's state hydrates. - A11y plumbing. The root is a
role="region"landmark with a configurablearia-label. A visually-hiddenaria-live="polite"region announces the active phase'slabelso screen readers track progress through the flow.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
phases | readonly OnboardingPhase[] | required | Ordered phase array. Each entry { id, label, content }. |
currentPhase | string | required | Active phase id. Must match one of phases[].id. |
onAdvance | (nextPhaseId: string | null) => void | required | Fires when the active phase wants to move forward. Receives the next id or null at the last phase. |
regionAriaLabel | string | "Onboarding" | Accessible name for the outer region landmark. |
className | string | — | Merged onto the outer <div> via cn(). |
OnboardingPhase
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. Used for keying + the currentPhase lookup. |
label | string | Human-readable name. Announced to assistive tech on phase change. |
content | ReactNode | (ctx) => ReactNode | Phase body. Render-fn form receives the phase context. |
OnboardingPhaseContext
| Field | Type | Description |
|---|---|---|
currentPhase | string | Active phase id. |
index | number | Active phase ordinal in phases. |
total | number | phases.length. |
isLast | boolean | True when this is the final phase. |
advance | () => void | Call to fire onAdvance with the next phase id (or null). |
Accessibility
- The root renders as a
role="region"landmark witharia-label(defaults to"Onboarding") so assistive tech can jump straight to the flow. - A visually-hidden
aria-live="polite"region announces the active phase'slabelwhenever it changes — screen-reader users hear the new phase name on each advance. - Cross-fade animations respect
prefers-reduced-motion: the transition collapses to a plain opacity swap with no translate. - The shell does NOT trap focus — that's the responsibility of the phase body. If the onboarding flow runs as a modal overlay, wrap it in a focus-trap (e.g. Radix
Dialog) inside the consumer. - The shell does NOT manage keyboard navigation between phases — the phase body owns its own controls (Continue buttons, skip links, etc.).
Credits
- Extracted from:
algoflashcards(src/platform/ui/Onboarding.tsx). The source baked in five hard-coded phases (showcase,play,victory,personalize,launch), a project-specificDataProviderfor content + mutations, theGameCardRouterruntime, track-color theming, and bespoke phase shells. craft-bits' version strips all of that: the shell becomes a generic phase-array runner with external state, leaving content composition (including every per-phase chrome decision) to the consumer.