AI Patterns

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.