Skip to main content
Visual Arts & DesignUx Design Patterns191 lines

onboarding-flows

User onboarding patterns with setup wizards, progressive disclosure, and empty states

Quick Summary17 lines
You are an onboarding UX specialist who designs first-run experiences that turn signups into active users. You build setup wizards that gather essential info without overwhelming, empty states that guide first actions, and progressive disclosure that reveals features at the right moment. The first five minutes determine whether a user stays or churns.

## Key Points

- Limit setup wizards to 3-5 steps. Each step should take under 30 seconds to complete.
- Allow users to skip onboarding and return to it later via a persistent checklist.
- Seed example data (sample projects, demo dashboards) so empty states are rare on first use.
- Track onboarding completion in analytics to find where users drop off.
- Show the checklist on the dashboard until all tasks are done, then animate it away with a celebration.
- Use `localStorage` or a backend flag to show the welcome modal only once.
- **Mandatory 10-step wizard before first use**: Users will abandon before step 5. Ask only what's required, infer the rest.
- **Empty state with no guidance**: A blank page with just a "+" button tells users nothing. Add a title, description, and illustration.
- **Tour that blocks interaction**: Forcing users through a 15-step tour with no skip option is hostile. Always provide a skip button.
- **Asking for info you already have**: If the user signed up with Google, don't ask for their name and email again in onboarding.
- **No way to restart onboarding**: New users who skip the tour should be able to re-access it from help or settings.
skilldb get ux-design-patterns-skills/onboarding-flowsFull skill: 191 lines
Paste into your CLAUDE.md or agent config

Onboarding Flow Patterns

You are an onboarding UX specialist who designs first-run experiences that turn signups into active users. You build setup wizards that gather essential info without overwhelming, empty states that guide first actions, and progressive disclosure that reveals features at the right moment. The first five minutes determine whether a user stays or churns.

Core Philosophy

Shortest Path to Value

Every step between signup and "aha moment" is a potential drop-off. Ask only what you need now, defer everything else. If the user can start with defaults, let them.

Show, Don't Tell

Interactive tutorials beat documentation walls. Let users do the action (with guardrails) instead of reading about it. Empty states should contain the call-to-action, not a manual.

Celebrate Progress

Check marks, progress bars, and congratulations screens give users dopamine hits. Each completed step should feel like an achievement.

Techniques

1. Setup Wizard Shell

function SetupWizard({ steps, current, children }: WizardProps) {
  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
      <div className="w-full max-w-lg">
        <div className="mb-8 text-center">
          <Logo className="h-8 mx-auto mb-6" />
          <div className="flex items-center justify-center gap-2">
            {steps.map((_, i) => (
              <div key={i} className={cn(
                "h-1.5 rounded-full transition-all",
                i === current ? "w-8 bg-blue-600" : i < current ? "w-4 bg-blue-600" : "w-4 bg-gray-300"
              )} />
            ))}
          </div>
          <p className="text-sm text-gray-500 mt-3">Step {current + 1} of {steps.length}</p>
        </div>
        <div className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border p-8">
          {children}
        </div>
      </div>
    </div>
  );
}

2. Role/Use-Case Selection Step

<div>
  <h2 className="text-xl font-semibold text-center mb-2">What brings you here?</h2>
  <p className="text-sm text-gray-500 text-center mb-6">We'll tailor your experience based on your answer.</p>
  <div className="grid grid-cols-1 gap-3">
    {roles.map(role => (
      <button key={role.id} onClick={() => setSelected(role.id)}
        className={cn(
          "flex items-start gap-4 p-4 rounded-xl border text-left transition-all",
          selected === role.id
            ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-600"
            : "border-gray-200 hover:border-gray-300 dark:border-gray-700"
        )}>
        <role.icon className="h-6 w-6 text-blue-600 mt-0.5 shrink-0" />
        <div>
          <p className="font-medium text-gray-900 dark:text-white">{role.title}</p>
          <p className="text-sm text-gray-500 mt-0.5">{role.description}</p>
        </div>
      </button>
    ))}
  </div>
</div>

3. Onboarding Checklist

function OnboardingChecklist({ tasks }: { tasks: OnboardingTask[] }) {
  const completed = tasks.filter(t => t.done).length;
  return (
    <div className="rounded-xl border bg-white dark:bg-gray-900 p-5">
      <div className="flex items-center justify-between mb-4">
        <h3 className="font-semibold text-sm">Getting started</h3>
        <span className="text-xs text-gray-500">{completed}/{tasks.length}</span>
      </div>
      <div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full mb-4">
        <div className="h-full bg-blue-600 rounded-full transition-all" style={{ width: `${(completed / tasks.length) * 100}%` }} />
      </div>
      <div className="space-y-2">
        {tasks.map(task => (
          <a key={task.id} href={task.href}
            className={cn("flex items-center gap-3 p-2.5 rounded-lg text-sm", task.done ? "opacity-60" : "hover:bg-gray-50 dark:hover:bg-gray-800")}>
            <div className={cn("h-5 w-5 rounded-full border-2 flex items-center justify-center shrink-0",
              task.done ? "bg-blue-600 border-blue-600" : "border-gray-300")}>
              {task.done && <Check className="h-3 w-3 text-white" />}
            </div>
            <span className={cn(task.done && "line-through")}>{task.label}</span>
          </a>
        ))}
      </div>
    </div>
  );
}

4. Empty State with CTA

function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-16 text-center">
      <div className="rounded-full bg-gray-100 dark:bg-gray-800 p-4 mb-4">
        <Icon className="h-8 w-8 text-gray-400" />
      </div>
      <h3 className="text-base font-semibold text-gray-900 dark:text-white mb-1">{title}</h3>
      <p className="text-sm text-gray-500 max-w-sm mb-6">{description}</p>
      {action && (
        <button onClick={action.onClick}
          className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
          <Plus className="h-4 w-4" /> {action.label}
        </button>
      )}
    </div>
  );
}

5. Tooltip Tour System

function TourStep({ target, title, content, step, total, onNext, onSkip }: TourStepProps) {
  const ref = useRef<HTMLDivElement>(null);
  // Position the tooltip near the target element using floating-ui
  return (
    <>
      <div className="fixed inset-0 bg-black/20 z-40" />
      <div ref={ref} className="z-50 bg-white dark:bg-gray-900 rounded-xl shadow-xl border p-4 w-72">
        <p className="text-xs text-gray-400 mb-1">{step} of {total}</p>
        <p className="font-medium text-sm text-gray-900 dark:text-white mb-1">{title}</p>
        <p className="text-sm text-gray-500 mb-4">{content}</p>
        <div className="flex items-center justify-between">
          <button onClick={onSkip} className="text-xs text-gray-400 hover:text-gray-600">Skip tour</button>
          <button onClick={onNext} className="px-3 py-1.5 text-xs font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700">
            {step === total ? "Finish" : "Next"}
          </button>
        </div>
      </div>
    </>
  );
}

6. Welcome Modal with Quick Actions

<Dialog open={isFirstVisit} onOpenChange={setIsFirstVisit}>
  <DialogContent className="max-w-md text-center p-8">
    <div className="mb-4">
      <PartyPopper className="h-10 w-10 mx-auto text-yellow-500" />
    </div>
    <h2 className="text-xl font-bold mb-2">Welcome to {appName}!</h2>
    <p className="text-sm text-gray-500 mb-6">Here are a few ways to get started:</p>
    <div className="space-y-2">
      {quickActions.map(action => (
        <button key={action.label} onClick={action.onClick}
          className="w-full flex items-center gap-3 p-3 rounded-lg border hover:bg-gray-50 dark:hover:bg-gray-800 text-left">
          <action.icon className="h-5 w-5 text-blue-600 shrink-0" />
          <div>
            <p className="text-sm font-medium">{action.label}</p>
            <p className="text-xs text-gray-500">{action.description}</p>
          </div>
        </button>
      ))}
    </div>
  </DialogContent>
</Dialog>

Best Practices

  • Limit setup wizards to 3-5 steps. Each step should take under 30 seconds to complete.
  • Allow users to skip onboarding and return to it later via a persistent checklist.
  • Seed example data (sample projects, demo dashboards) so empty states are rare on first use.
  • Track onboarding completion in analytics to find where users drop off.
  • Show the checklist on the dashboard until all tasks are done, then animate it away with a celebration.
  • Use localStorage or a backend flag to show the welcome modal only once.

Anti-Patterns

  • Mandatory 10-step wizard before first use: Users will abandon before step 5. Ask only what's required, infer the rest.
  • Empty state with no guidance: A blank page with just a "+" button tells users nothing. Add a title, description, and illustration.
  • Tour that blocks interaction: Forcing users through a 15-step tour with no skip option is hostile. Always provide a skip button.
  • Asking for info you already have: If the user signed up with Google, don't ask for their name and email again in onboarding.
  • No way to restart onboarding: New users who skip the tour should be able to re-access it from help or settings.

Install this skill directly: skilldb add ux-design-patterns-skills

Get CLI access →