Skip to content

Create onboarding flows that people finish

Ask for less up front, show progress honestly, and let people skip, resume and finish setup on their own schedule.

On this page
  1. Ask for less up front
  2. Checklist or wizard
  3. A short wizard
  4. A checklist people return to
  5. Show progress honestly
  6. Skip, resume and remember
  7. Empty states that teach
  8. Celebrate once

Ask for less up front

Every question before someone sees your product is a reason to leave. Onboarding that people finish starts by sorting each step into one of three piles:

  • Required to start: usually an account and a name for the workspace. Ask for these now.
  • Required to get value: connecting data, inviting the team. Ask for these once people can see why.
  • Nice to have: a photo, a time zone, preferences. Infer them, default them, or ask later.

Most setup flows can shrink to one or two screens this way. The rest moves into the product, where people choose when to do it.

Checklist or wizard

A wizard shows one step at a time in a fixed order. Use it when a later step depends on an earlier one, like picking a plan before entering payment, and keep it to three or four steps.

A checklist lists the steps and lets people do them in any order, over days if they like. Use it for setup that can happen inside the product: connecting tools, inviting people, trying a key feature.

Many products use both: a short wizard to create the workspace, then a checklist inside it for everything else.

Terminal
npx shadcn@latest add @uiarc/stepper @uiarc/onboarding-checklist @uiarc/progress

A short wizard

Arc’s stepper shows where people are and how much is left. You own the content and the current index, so the steps can be anything. Passing onStepSelect lets people click back to a finished step to fix it.

The name is required and validated on Continue. The teammate step can be skipped. Back keeps what you entered.
setup.tsx
import { Stepper } from "@/registry/components/stepper/stepper"; const steps = [  { id: "name", label: "Workspace", description: "Required" },  { id: "team", label: "Teammates", description: "Optional" },  { id: "start", label: "Starting point", description: "Required" },]; export function Setup() {  const [current, setCurrent] = useState(0);  return (    <>      <Stepper        steps={steps}        current={current}           // steps.length marks the flow complete        onStepSelect={setCurrent}   // lets people go back to finished steps        label="Workspace setup"      />      {/* render the content for steps[current] */}    </>  );}

Validate when people press Continue, and say what to do, not just what is wrong. Keep Back always available and never clear a field when someone goes back. Label the last button with what it does, like “Create workspace”, not “Finish”.

A checklist people return to

Arc’s onboarding checklist is built for three to eight steps. Each step can carry its own action button, so people do the step right there instead of hunting for the setting it refers to.

Get started with Northwind

1 of 4 done

  • Previews build from every pull request.

Connect GitHub fails the first time to show the error path, then works. All actions are simulated.
getting-started.tsx
import { OnboardingChecklist, type OnboardingStep } from "@/registry/components/onboarding-checklist/onboarding-checklist"; const steps: OnboardingStep[] = [  { id: "photo", title: "Add your photo" },  {    id: "repo",    title: "Connect a repository",    description: "Previews build from every pull request.",    actionLabel: "Connect GitHub",    // Resolve to check the step off. Throw to keep it open with the message.    onAction: async () => {      const ok = await connectGitHub();      if (!ok) throw new Error("GitHub didn’t respond. Try again.");    },  },  { id: "invite", title: "Invite a teammate", actionLabel: "Send invite", onAction: openInviteDialog },]; <OnboardingChecklist  title="Get started"  doneTitle="Your workspace is ready"  steps={steps}  defaultCompleted={user.onboarding.completed}  onCompletedChange={completed => saveOnboarding({ completed })}  defaultHidden={user.onboarding.hidden}  onHiddenChange={hidden => saveOnboarding({ hidden })}/>

When an action resolves, the step checks off, folds into the completed group, and the next open step expands and takes focus. When it throws, the step stays open and shows the error’s message, so people know what happened and can retry.

Show progress honestly

Progress keeps people going only when it is true. A few rules keep it that way:

  • Count steps, not screens, and count only steps people must do. A bar that sits at 90 percent for three more screens teaches people to distrust it.
  • Check a step off when the thing actually happened: the repository is connected, the invite was sent. Not when someone clicked past it.
  • Pre-complete what people already did elsewhere. If they signed up with a profile photo, that step starts done.

For progress outside the checklist, like a setup banner, a plain bar is enough:

setup-banner.tsx
import { Progress } from "@/registry/components/progress/progress"; <Progress value={completed.length} max={steps.length} label="Setup" showValue />

Skip, resume and remember

People leave in the middle of setup, and that is fine if they can pick up where they left off. Store which steps are done and whether the checklist is hidden on the account, so it survives a new device. The checklist reports both through onCompletedChange and onHiddenChange, and reads them back through defaultCompleted and defaultHidden.

onboarding.ts
// Store progress on the account, not in the browser, so it follows people across devices.type Onboarding = { completed: string[]; hidden: boolean; step?: number }; async function saveOnboarding(patch: Partial<Onboarding>) {  await fetch("/api/me/onboarding", {    method: "PATCH",    headers: { "content-type": "application/json" },    body: JSON.stringify(patch),  });}

Let people hide the checklist without finishing it. A checklist that cannot be dismissed turns into a banner people learn to ignore. Keep a way back to it, like a “Getting started” item in the help menu.

Empty states that teach

The first time someone opens a project list, it is empty. That screen is onboarding too. Replace “No projects” with one sentence about what a project is for and one button that creates the first one. If a sample project helps people understand the product, offer it as a choice, clearly labeled, and easy to delete.

Arc’s empty state component covers the layout: a short title, a line of context, and a primary action.

Celebrate once

Finishing setup deserves a moment. The checklist changes its title when the last step is done, and that is usually enough. If you add more, like a short animation, show it once, keep it under a second, skip it with reduced motion, and never block the next action behind it.

Then get out of the way. The best end to onboarding is the product, already set up the way people asked for.

More guides