Onboarding checklist
A setup checklist with progress ring and inline actions.
components/blocks/onboarding-checklist.tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Check, ChevronDown, PartyPopper } from "lucide-react";
import { ActionButton } from "@/components/ui/action-button";
import { ProgressRing } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
const STEPS = [
{ id: "profile", title: "Complete your profile", body: "Add a name and photo so teammates know who's who.", cta: "Add details" },
{ id: "invite", title: "Invite your team", body: "Projects move faster with people. Invite up to 3 teammates on the free plan.", cta: "Send invites" },
{ id: "connect", title: "Connect a data source", body: "Pull events from Segment, Stripe or your database to light up your dashboards.", cta: "Connect Stripe" },
{ id: "dashboard", title: "Build your first dashboard", body: "Start from a template or drag charts onto a blank canvas.", cta: "Use a template" },
{ id: "alert", title: "Set up an alert", body: "Get a Slack message when a metric moves more than you expect.", cta: "Create alert" },
];
export function OnboardingChecklist() {
const [done, setDone] = React.useState<string[]>(["profile", "invite"]);
const [open, setOpen] = React.useState<string | null>("connect");
const complete = done.length === STEPS.length;
const finish = (id: string) => {
setDone((d) => [...d, id]);
const next = STEPS.find((s) => s.id !== id && !done.includes(s.id));
setOpen(next?.id ?? null);
};
return (
<div className="mx-auto w-full max-w-xl overflow-hidden rounded-xl border border-border bg-background">
<header className="flex items-center gap-4 border-b border-border p-5">
<ProgressRing value={done.length} max={STEPS.length} size={52} aria-label="Setup progress">
{done.length}/{STEPS.length}
</ProgressRing>
<div>
<h2 className="font-semibold">{complete ? "You're all set" : "Get set up"}</h2>
<p className="text-sm text-muted-foreground">{complete ? "Everything's connected. Time to ship." : `${STEPS.length - done.length} steps left, about 6 minutes.`}</p>
</div>
</header>
<AnimatePresence mode="wait" initial={false}>
{complete ? (
<motion.div key="done" initial={{ opacity: 0, scale: 0.96 }} animate={{ opacity: 1, scale: 1 }} className="flex flex-col items-center gap-3 px-6 py-12 text-center">
<motion.span initial={{ rotate: -20, scale: 0.5 }} animate={{ rotate: 0, scale: 1 }} transition={{ type: "spring", bounce: 0.5 }} className="grid size-12 place-items-center rounded-full bg-emerald-500/15 text-emerald-500">
<PartyPopper className="size-6" />
</motion.span>
<p className="font-medium">Setup complete</p>
<button type="button" onClick={() => { setDone(["profile", "invite"]); setOpen("connect"); }} className="text-sm text-muted-foreground underline underline-offset-4">Start over</button>
</motion.div>
) : (
<motion.ol key="list" exit={{ opacity: 0 }} className="divide-y divide-border">
{STEPS.map((s) => {
const isDone = done.includes(s.id);
const isOpen = open === s.id && !isDone;
return (
<li key={s.id}>
<button type="button" aria-expanded={isOpen} disabled={isDone} onClick={() => setOpen(isOpen ? null : s.id)} className="flex w-full items-center gap-3 px-5 py-3.5 text-left text-sm disabled:cursor-default">
<motion.span
className={cn("grid size-5 shrink-0 place-items-center rounded-full border", isDone ? "border-emerald-500 bg-emerald-500 text-white" : "border-input")}
animate={isDone ? { scale: [1, 1.25, 1] } : { scale: 1 }}
transition={{ duration: 0.3 }}
>
{isDone && <Check className="size-3" />}
</motion.span>
<span className={cn("flex-1 font-medium", isDone && "text-muted-foreground line-through decoration-muted-foreground/40")}>{s.title}</span>
{!isDone && <ChevronDown className={cn("size-4 text-muted-foreground transition-transform", isOpen && "rotate-180")} />}
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ type: "spring", bounce: 0, duration: 0.3 }} className="overflow-hidden">
<div className="flex flex-wrap items-center justify-between gap-3 px-5 pb-4 pl-13 text-sm">
<p className="max-w-sm text-muted-foreground">{s.body}</p>
<ActionButton className="h-9 px-3" pendingLabel="Working" successLabel="Done" resetAfter={600} onAction={async () => { await new Promise((r) => setTimeout(r, 700)); setTimeout(() => finish(s.id), 600); }}>
{s.cta}
</ActionButton>
</div>
</motion.div>
)}
</AnimatePresence>
</li>
);
})}
</motion.ol>
)}
</AnimatePresence>
</div>
);
}
export default OnboardingChecklist;What's inside
- Progress ring
- Inline actions per step
- Completion state
When to use
- First-run setup in a SaaS dashboard.
When not to use
- Linear forms: use a stepper.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/onboarding-checklist.jsonLands in components/blocks/onboarding-checklist.tsx with sample data you replace with your own.
The CLI also installs motion, lucide-react and every Lode component it's built from.
Usage
Import it from @/components/blocks/onboarding-checklist after installing. It ships with sample data, so it renders straight away; swap in your own data when you're ready.
example.tsx
import { OnboardingChecklist } from "@/components/blocks/onboarding-checklist";
export default function Page() {
return (
<main className="mx-auto max-w-6xl p-6">
<OnboardingChecklist />
</main>
);
}API reference
OnboardingChecklist
Five setup steps with a progress ring, inline actions per step and a completion state. Also the default export. No props: sample data sits at the top of the file, ready to swap for your own.
No props.
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Move between steps and actions |
| EnterorSpace | Expand a step or run its action |
Accessibility
- Steps are buttons with aria-expanded.
- Progress is a labelled progressbar.
Motion
- Completed steps check off and the ring fills.
Responsive behavior
- Single column at every width.
Performance
- Sample data is plain arrays at the top of the file. Replace them with your API data and keep the shapes.
Notes for AI
- Persist completed steps on your server so the list survives reloads.
This page as Markdown, for assistants and coding agents:
/blocks/onboarding-checklist/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt