Patterns

Onboarding

First-run flows that welcome a new user — without marching anyone through a tutorial they didn't ask for. Keep it warm and quick: a friendly hello, the levers that matter, and a clear way out. Onboarding is opt-in, dismissible, and skippable at every step.

This recipe is Beta. The three models and the compositions below are settled; the per-feature copy library, the stepped-flow choreography, the coach-mark dismissal-and-recall mechanics, and A/B-testable variants are pending and will be authored as onboarding flows are built. The stepper rail shown here is demo CSS — a real Stepper component is a named gap.

Principles

  • Respect the user's time. No forced tutorials. Every onboarding surface can be dismissed now and finished later — the product never holds work hostage behind a walkthrough.
  • Show, don't tell. One example the user can act on beats a wall of explanation. If a step needs three paragraphs, the step is wrong, not the copy.
  • Stepped or contextual — not both. Choose one model per feature: a discrete multi-step flow, or coaching that surfaces as the user encounters things. Running both doubles the interruptions and halves the trust.
  • Progress is visible. If the flow is stepped, the user always knows how many steps remain — a numbered rail plus a plain "Step n of N" line, never a mystery spinner of screens.
  • Skip is always visible. A hidden skip control is a dark pattern. Skip is a first-class, keyboard-reachable button on every step — not small print in a corner.
  • This audience knows their way around. A compliance pro does not want hand-holding — fun lives in the surfaces, not in extra steps. Collect what setup actually needs, then hand over the keys.

Three models, choose one

Every onboarding job fits one of three shapes. Pick by what the moment needs — baseline information, a pointer, or simply a first action.

Stepped onboarding
A discrete N-step flow for first sign-in, when baseline info must exist before the product is useful. Full-screen takeover; clear progress; skip on every step.
Contextual coaching
A small inline callout next to a feature the first time the user encounters it. Dismissed once, gone forever — never a guided tour in disguise.
Empty-state-as-onboarding
The feature's first-run empty state is its onboarding moment; the empty slot's CTA is the user's first action. No extra surface at all.

Stepped onboarding

For first sign-in, when the product needs baseline information before it can do its job. Each step collects one thing; the rail shows where you are; Skip stays visible from the first step to the last. The final step is a handoff, not a finale.

Stepped flow

Step 1 of 3

  1. Organization
  2. Jurisdictions
  3. Handoff

Confirm your organization

Check the legal name and primary contact we have on file. Reviews are issued under this name.

A composition, not a component: an ordered list carries the rail (aria-current="step" on the active step, a check glyph on completed ones — never color alone), each step swaps in on a flat, friendly surface, and Skip never leaves the header. The last step hands off into the product.

Contextual coaching

For a feature with non-obvious affordances, or after a change that moves something existing users rely on. One inline callout, next to the thing itself, the first time the user meets it. Dismissing it is permanent — a coach mark that comes back is nagging, not coaching.

Coach mark
Review queue
New

Calibration sets how strict checks run. The default profile is a great place to start — tweak it once a few reviews have come through.

Live: the callout sits in the page flow next to the feature it explains — it never steals focus and never floats over content. Got it dismisses it for good; in product, the dismissal sticks per user.

The empty-state handoff

The third model needs no surface of its own: the feature’s first-run empty state is its onboarding, and the empty slot’s CTA is the user’s first action. The recipe lives at /empty-states — this page only adds the handoff rule: a stepped flow ends by landing the user in front of that empty state, not on a dashboard tour. The flow collects what the product needs; the empty state teaches the first move.

Accessibility

  • A real list, in real order. The stepper rail is an ordered list; the active step carries aria-current="step" and progress also appears as plain text ("Step 2 of 3"). State is never color alone — completed steps draw a check glyph (ADR-0017).
  • Announce the step change. The step pane is aria-live="polite", so assistive tech hears the new step without losing its place. A full-screen takeover manages focus like a Modal — trapped while open, returned on exit.
  • Skip is a real button. Keyboard-focusable, visible focus ring, present on every step. If a sighted user can see it, a keyboard user can reach it — and vice versa.
  • Coach marks never steal focus. The callout is inline content, not a focus-grabbing popover. Dismissal works from the keyboard, and the dismissed state persists so it never has to be re-dismissed.
  • Reduced motion. The step pane's entrance animation is skipped under prefers-reduced-motion; step changes become instant swaps.

Usage

The pattern is a composition, not a component — there is no Stepper or OnboardingFlow export yet, and the rail below is page-level CSS. That gap is deliberate until a second consumer appears; compose Card, Text, Button, and Badge in place.

Stepped flow inside a Cardtsx
import { ArrowRight, Check } from 'lucide-react'
import { Button, Card, Text } from '@halo-compliance/ui'

<Card variant="secondary">
  <Card.Body>
    {/* header: progress text + the always-visible skip */}
    <Text variant="overline" as="p">Step 2 of 3</Text>
    <Button variant="ghost" tone="secondary" onClick={skip}>
      Skip setup
    </Button>

    {/* the rail: a real ordered list, state never color-alone */}
    <ol className="wizard-steps">
      <li className="is-done">
        <Check aria-hidden="true" /> Organization
      </li>
      <li className="is-active" aria-current="step">Jurisdictions</li>
      <li className="is-upcoming">Handoff</li>
    </ol>

    {/* one step, one thing; announced politely on change */}
    <div aria-live="polite">
      <Text variant="h4" as="h3">Choose your jurisdictions</Text>
      <Text variant="body-sm" as="p">
        Pick the states where you operate.
      </Text>
    </div>

    <Button
      variant="primary"
      icon={<ArrowRight aria-hidden="true" />}
      onClick={next}
    >
      Continue
    </Button>
  </Card.Body>
</Card>

Anti-patterns

  • Don't force the tour. A tutorial the user cannot leave is a lock on the front door. Every step is skippable; the product must work for a user who skipped everything.
  • Don't hide skip. Low-contrast text in a far corner that appears after a delay is a dark pattern, not a design choice. Skip is a first-class button from the first frame.
  • Don't run both models. A stepped flow followed by a trail of coach marks means the user is onboarded twice and trusted zero times. One model per feature.
  • Don't market inside onboarding. Banners promoting features are a different pattern with different rules. Onboarding orients the user to what they already bought — it never sells.
  • /empty-states — the third model in full: what the first-run slot says and does.
  • /page-header — the orientation band a wizard’s Steps mount around.
  • /modal — the focus-trap behavior a full-screen takeover inherits.
  • /card — the surface that hosts each step.
  • /button — Skip, Back, and Continue are all canonical Buttons.