Skip to content
All Taste components

Onboarding

Onboarding checklist

A getting-started list that shows progress, what is done and the one step to do next.

progress bar · done · next (expanded) · to do · dismiss

Get Saffron Studio ready

3 of 5 done · about 4 minutes left

  1. Create your workspace
  2. Add your services and prices
  3. Connect a payout account
  4. Invite your team

    Give stylists their own calendar so clients can book them directly.

  5. Share your booking page
import { Check, Plus, X } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SetupStep { id: string; title: string; hint?: string; done?: boolean; action?: string }

const sample: SetupStep[] = [
  { id: "workspace", title: "Create your workspace", done: true },
  { id: "services", title: "Add your services and prices", done: true },
  { id: "payouts", title: "Connect a payout account", done: true },
  { id: "team", title: "Invite your team", hint: "Give stylists their own calendar so clients can book them directly.", action: "Invite people" },
  { id: "share", title: "Share your booking page", hint: "Copy your link or add the button to your website.", action: "Copy link" },
];

export function OnboardingChecklist({ title = "Get Saffron Studio ready", steps = sample, onAction, onSkip, onDismiss }: {
  title?: string;
  steps?: SetupStep[];
  onAction?: (id: string) => void;
  onSkip?: (id: string) => void;
  onDismiss?: () => void;
}) {
  const done = steps.filter((s) => s.done).length;
  const pct = Math.round((done / steps.length) * 100);
  const next = steps.find((s) => !s.done);

  return (
    <section className="w-full max-w-md rounded-lg border border-border bg-bg p-4">
      <div className="flex items-start justify-between gap-3">
        <div>
          <h3 className="text-sm font-medium">{title}</h3>
          <p className="text-[12.5px] text-fg-subtle">{done} of {steps.length} done</p>
        </div>
        <button onClick={onDismiss} aria-label="Hide checklist" className="grid size-7 place-items-center rounded text-fg-muted hover:bg-surface-2">
          <X className="size-4" />
        </button>
      </div>
      <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-surface-2" role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label="Setup progress">
        <div className="h-full rounded-full bg-primary transition-[width]" style={{ width: pct + "%" }} />
      </div>

      <ol className="mt-4 grid gap-0.5">
        {steps.map((s) => {
          const isNext = s.id === next?.id;
          return (
            <li key={s.id} className={cn("grid grid-cols-[22px_minmax(0,1fr)] gap-3 rounded px-3 py-2.5 text-[13.5px]", isNext && "bg-surface")}>
              <span className={cn("grid size-[22px] place-items-center rounded-full",
                s.done ? "bg-primary text-primary-fg" : isNext ? "border-[1.5px] border-primary" : "border-[1.5px] border-dashed border-border-strong")}>
                {s.done && <Check className="size-3" aria-hidden />}
              </span>
              <div>
                <span className={cn("font-medium", s.done && "text-fg-subtle line-through")}>
                  {s.title}{s.done && <span className="sr-only"> (done)</span>}
                </span>
                {isNext && (
                  <>
                    {s.hint && <p className="mb-2.5 mt-0.5 text-[13px] text-fg-muted">{s.hint}</p>}
                    <div className="flex flex-wrap gap-2">
                      <button onClick={() => onAction?.(s.id)} className="inline-flex h-[30px] items-center gap-1.5 rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">
                        <Plus className="size-3.5" aria-hidden />{s.action ?? "Start"}
                      </button>
                      <button onClick={() => onSkip?.(s.id)} className="h-[30px] rounded px-2.5 text-[13px] font-medium hover:bg-surface-2">Skip for now</button>
                    </div>
                  </>
                )}
              </div>
            </li>
          );
        })}
      </ol>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.