navigation
Setup Checklist
A stepped onboarding card with circular arc progress, dashed-circle pending states, and strikethrough for completed steps.
Set up Computer
1/3
Click a step to complete it
"use client";
import * as React from "react";
import { motion } from "motion/react";
import { Check, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
export type SetupStepStatus = "pending" | "done";
export interface AppIcon {
label: string;
/** Tailwind-compatible hex or CSS color string for the icon background. */
color: string;
initial: string;
}
export interface SetupStep {
id: string;
label: string;
status: SetupStepStatus;
/** Optional small app icons shown to the right of the label row. */
icons?: AppIcon[];
onClick?: () => void;
}
export interface SetupChecklistProps {
title: string;
steps: SetupStep[];
className?: string;
}
export function SetupChecklist({ title, steps, className }: SetupChecklistProps) {
const doneCount = steps.filter((s) => s.status === "done").length;
const total = steps.length;
return (
<div className={cn("rounded-2xl bg-muted/70 p-3", className)}>
<div className="mb-3 flex items-center justify-between px-1">
<span className="text-xs font-medium text-foreground">{title}</span>
<div className="flex items-center gap-2">
<span className="text-xs tabular-nums text-muted-foreground">
{doneCount}/{total}
</span>
<CircularProgress done={doneCount} total={total} />
</div>
</div>
<div className="overflow-hidden rounded-xl bg-card">
{steps.map((step, index) => (
<React.Fragment key={step.id}>
{index > 0 && <div className="mx-4 h-px bg-border/50" />}
<SetupRow step={step} />
</React.Fragment>
))}
</div>
</div>
);
}
function CircularProgress({ done, total }: { done: number; total: number }) {
const size = 22;
const strokeWidth = 2;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const progress = total === 0 ? 0 : done / total;
const strokeDashoffset = circumference * (1 - progress);
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
aria-hidden
style={{ transform: "rotate(-90deg)" }}
>
{/* Track */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-border"
/>
{/* Progress arc */}
<motion.circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
initial={false}
animate={{ strokeDashoffset }}
transition={{ duration: 0.45, ease: "easeOut" }}
className="text-foreground"
/>
</svg>
);
}
function SetupRow({ step }: { step: SetupStep }) {
const done = step.status === "done";
return (
<button
type="button"
onClick={done ? undefined : step.onClick}
disabled={done}
className={cn(
"flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors",
!done && "cursor-pointer hover:bg-muted/50",
done && "cursor-default"
)}
>
<StepIcon status={step.status} />
<span
className={cn(
"flex-1 text-xs font-medium",
done ? "text-muted-foreground line-through decoration-muted-foreground/60" : "text-foreground"
)}
>
{step.label}
</span>
{step.icons && step.icons.length > 0 && !done && (
<div className="flex items-center gap-0.5" aria-hidden>
{step.icons.map((icon) => (
<span
key={icon.label}
title={icon.label}
className="flex size-[18px] items-center justify-center rounded-md text-[9px] font-bold text-white"
style={{ backgroundColor: icon.color }}
>
{icon.initial}
</span>
))}
</div>
)}
{!done && <ChevronRight className="size-4 shrink-0 text-muted-foreground" aria-hidden />}
</button>
);
}
function StepIcon({ status }: { status: SetupStepStatus }) {
if (status === "done") {
return (
<motion.span
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="flex size-5 shrink-0 items-center justify-center rounded-full bg-muted-foreground/25"
aria-label="Completed"
>
<Check className="size-3 text-muted-foreground" strokeWidth={2.5} aria-hidden />
</motion.span>
);
}
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
aria-label="Pending"
className="shrink-0"
>
<circle
cx="10"
cy="10"
r="8"
stroke="currentColor"
strokeWidth="1.5"
strokeDasharray="2.8 2.2"
strokeLinecap="round"
className="text-muted-foreground/40"
/>
</svg>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Setup Checklist
## Summary
A compact onboarding card that surfaces a short list of setup steps, tracks progress with a circular arc indicator, and lets users work through each step sequentially or in any order. Completed steps collapse into struck-through, muted rows while remaining steps stay actionable.
## When to use
- First-run onboarding for AI agents, desktop apps, or workspaces (e.g., "Set up Computer", "Connect your tools").
- Post-signup activation flows where completing each step unlocks value (connected integrations, notifications, first task).
- Re-surfacing incomplete setup inside a dashboard or sidebar when the user still has steps pending.
## When not to use
- Long multi-step wizards (more than ~5 steps) — use a dedicated onboarding screen with a stepper instead.
- Flows where steps must be completed in strict sequence and blocking is required — the checklist pattern implies optional ordering.
- Critical setup that must gate the core experience — surface a blocking modal instead.
## Anatomy
- **Header bar**: title (product or object being set up) on the left; step counter (e.g., "1 / 3") and circular arc progress indicator on the right.
- **Step list**: a card containing one row per step, separated by hairline dividers.
- **Step row**: status icon + label + optional app-icon cluster + chevron (pending only).
- **Status icon — pending**: dashed-stroke circle (conveys "not yet started" without implying failure).
- **Status icon — done**: filled muted circle with a checkmark; label gains line-through decoration.
- **App icons**: small colored squares shown for steps tied to integrations; hidden once the step is done.
- **Chevron**: right-pointing arrow on actionable rows; removed once done.
## Behavior
- Clicking a pending row triggers that step's action (e.g., opens an OAuth sheet or a settings panel) and, on success, marks it done.
- Marking a step done animates its status icon (spring scale-in) and immediately updates the circular arc.
- The arc animates to the new progress value with a short ease-out tween each time a step is completed.
- All steps done: show a completion affordance (e.g., confetti, success state, or a "Replay" button in demos).
- Steps may be completed in any order unless the product constrains it; the component does not enforce ordering.
## Content guidelines
- Title: short verb phrase naming the object being configured — "Set up Computer", "Connect your workspace". Not "Onboarding".
- Step labels: imperative sentence fragments starting with a verb — "Connect your apps", "Turn on notifications". Max ~35 characters so they fit on one line.
- App icons: max 3–4; beyond that they become unreadable. Use brand colors for recognition.
## Accessibility
- The progress arc is decorative (`aria-hidden`); the counter text ("1/3") is the accessible progress signal.
- Each step row must be a real `<button>` element; disabled when done so it is skipped by keyboard navigation.
- Status icons carry `aria-label` ("Completed" / "Pending") for screen reader users.
- The step list should be wrapped in an `aria-live` region so screen readers announce completion events.
## Related patterns
- Analysis List — sequential item resolution during an AI analysis run, not user-driven.
- Tool Approval — one-at-a-time permission prompts, not a persistent checklist.