The frame for a multi-step onboarding flow: segmented progress, one decision per screen, Back, Skip and the primary action, and an optional live preview beside it. It draws a flow from useSteps, the headless step engine in Onboarding core.
When to use
- A first-run flow of three to six questions: profile, workspace, team, preferences.
- Any flow where each screen asks one thing and the answers build on each other.
- When you want the step logic (validity, branching, skip, async steps, saved progress) without writing it, and the keyboard and screen reader behaviour done.
When not to use
- A checklist people come back to over days: use Onboarding Checklist.
- One form that fits on a screen: a plain form with Field is faster to fill.
Installation
npx shadcn add @eased-pro/onboarding-shellNeeds the @eased-pro registry and your Pro token in components.json. Create a token and copy the setup from your account .
Usage
import { useSteps } from "@/components/eased/_primitives/onboarding-core/onboarding-core";import { OnboardingShell } from "@/components/eased/onboarding-shell/onboarding-shell";import { Field } from "@/components/eased/field/field";import { Input } from "@/components/eased/input/input";export function ProfileSetup() { const flow = useSteps({ steps: [ { id: "name", title: "What should your team call you?", valid: (p) => p.name.trim() !== "" }, { id: "role", title: "What’s your role?", optional: true }, ], initialData: { name: "", role: "" }, onComplete: (profile) => saveProfile(profile), }); return ( <OnboardingShell flow={flow} done={<h2>You’re set, {flow.data.name}</h2>} steps={{ name: ( <Field label="Full name"> <Input value={flow.data.name} onChange={(e) => flow.setData({ name: e.target.value })} /> </Field> ), role: <RolePicker value={flow.data.role} onChange={(role) => flow.setData({ role })} />, }} /> );}Examples
A live preview
Pass preview to show what is being set up beside the steps once the shell is 48rem wide, and compactPreview for a smaller version above the heading below that. Workspace Setup is the full example.
<OnboardingShell flow={flow} preview={<AppPreview draft={flow.data} focus={flow.id} />} compactPreview={<Badge draft={flow.data} />}>An async step
onNext on a step runs before the move. Return a promise and the primary button shows a spinner; throw and the step stays open with the message above the footer.
{ id: "invite", title: "Invite your team", onNext: (d) => api.invite(d.emails) }Saved progress
useSteps({ steps, initialData, persist: { key: "kestrel:profile" } });API reference
OnboardingShell
| Prop | Type | Default | Description |
|---|---|---|---|
flow | Steps<Data, Id, S> | From useSteps. The shell keeps no state of its own. | |
steps | { [K in Id]: ReactNode } | Each step's content, keyed by the flow's ids: a typo or a missing step fails to compile. Only the open step renders, and it keeps rendering while it leaves. | |
preview | ReactNode | Beside the steps from 48rem of container width. | |
compactPreview | ReactNode | Above the heading below 48rem, until the flow completes. | |
done | ReactNode | Replaces the steps once complete. Give it a heading: focus moves there. | |
labels | Partial<OnboardingLabels> | Continue, Finish, Back, Skip, "2 of 4" | A step's action overrides the primary label. |
manageFocus | boolean | (() => boolean) | true | Move focus to each new step's heading. For a scripted demo pass useAutoplay's byVisitor: asked once per step change, it moves focus only when the visitor caused the change, never for the script. When focus stays, the live region announces the step instead. |
className | string | "" | The root is a size container and a flex column; give it a height. |
The step engine (useSteps, StepDef, webStorage, useAutoplay) is documented in the Onboarding core primitive, installed with the shell.
Keyboard
| Key | Action |
|---|---|
Enter | Continues when the step is valid, from a field, a radio, the heading or anywhere in the form that is not a button or link. |
Escape | Goes back, unless a menu, listbox or dialog inside the step has it. |
Tab | Moves through the step, then Back, Skip and the primary action. |
Accessibility
- The bar is a
progressbarvalued in steps, with "Step 2 of 4: Pick your URL" as its text. - Each step is announced once. When the person moves, focus goes to the new step's heading (never on first load), which reads the question. When focus does not move (
manageFocusoff, or a script's move), a polite live region that stays mounted says "Step 2 of 4: Pick your URL" instead. - Back keeps its place on the first step, invisible, so the primary action never moves. A disabled Continue stays focusable.
- An
onNextoronCompletefailure is shown above the footer in arole="alert"line.
Motion
Steps change in sequence, never cross-faded: the outgoing step leaves 4px against the direction of travel in 120ms on the exit curve, then the next arrives from 8px along it over 200ms on the enter curve, so forward moves left and Back moves right. Each segment of the bar fills from the left on smooth and empties when you go back past it (transform only). The primary label swaps with a blur while its width springs ("Continue" → "Create workspace"). Reduced motion: steps cross in 100ms of opacity and the bar jumps.
Notes for AI
Use it for any first-run flow with one question per screen. Create the flow with useSteps, put validation in each step's valid, async work in onNext or onComplete, and branch with when; the shell reads all of it. Use preview for a live result beside the steps.
Don't add your own progress bar, step counter, Back or Continue buttons, step headings or focus handling: the shell draws them from the step definitions. Don't render steps conditionally yourself; pass each in steps so the exit animation keeps the outgoing content.
Related
- Workspace Setup: a full flow on this shell, with a live preview.
- Onboarding Checklist: setup tasks inside the product.