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

Welcome

A three-phase onboarding flow. The shell cross-fades between phases; you own the state.

Customize
A11y

Installation

npx shadcn@latest add https://craftbits.dev/r/onboarding.json

Usage

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

  1. External state. currentPhase is the truth — the shell does not own it. The consumer reacts to onAdvance(nextPhaseId) to update its own state. nextPhaseId is null when the last phase tried to advance.
  2. Phase array drives order. The order in phases IS the progression order. onAdvance is fired with phases[index + 1].id. Reordering the array reorders the flow.
  3. Cross-fade transitions. Each phase enters with a fade and exits with a fade plus a small upward translate. AnimatePresence runs in mode="wait" so the exiting phase clears before the next one mounts. Respects prefers-reduced-motion — the transition collapses to a plain opacity swap.
  4. Render-fn content. content may be ReactNode or (ctx) => ReactNode. The context exposes advance, isLast, index, total, and the resolved currentPhase so the body can wire its own progression UI without lifting state.
  5. Fallback for unknown ids. When currentPhase doesn't match any phases[].id, the shell renders the first phase as a safety net — common during initial load before the consumer's state hydrates.
  6. A11y plumbing. The root is a role="region" landmark with a configurable aria-label. A visually-hidden aria-live="polite" region announces the active phase's label so screen readers track progress through the flow.

Props

PropTypeDefaultDescription
phasesreadonly OnboardingPhase[]requiredOrdered phase array. Each entry { id, label, content }.
currentPhasestringrequiredActive phase id. Must match one of phases[].id.
onAdvance(nextPhaseId: string | null) => voidrequiredFires when the active phase wants to move forward. Receives the next id or null at the last phase.
regionAriaLabelstring"Onboarding"Accessible name for the outer region landmark.
classNamestringMerged onto the outer <div> via cn().

OnboardingPhase

FieldTypeDescription
idstringStable identifier. Used for keying + the currentPhase lookup.
labelstringHuman-readable name. Announced to assistive tech on phase change.
contentReactNode | (ctx) => ReactNodePhase body. Render-fn form receives the phase context.

OnboardingPhaseContext

FieldTypeDescription
currentPhasestringActive phase id.
indexnumberActive phase ordinal in phases.
totalnumberphases.length.
isLastbooleanTrue when this is the final phase.
advance() => voidCall to fire onAdvance with the next phase id (or null).

Accessibility

  • The root renders as a role="region" landmark with aria-label (defaults to "Onboarding") so assistive tech can jump straight to the flow.
  • A visually-hidden aria-live="polite" region announces the active phase's label whenever 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-specific DataProvider for content + mutations, the GameCardRouter runtime, 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.