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