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
- Create your workspace
- Add your services and prices
- Connect a payout account
- Invite your team
Give stylists their own calendar so clients can book them directly.
- 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.