All Taste components
Animated app UI
Onboarding stepper
A setup progress card where finished steps tick off, the line fills to where you are, and the current step gently pulses.
4 steps · done / current / upcoming · progress fill · breathing ring · step form
Set up your workspace
Step 3 of 4- Create account
- Add your business
- 3Invite your team
- 4Connect bank
Invite your teamThey’ll get an email to join Northwind. You can change roles later.
import { Check, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
/* Add once to globals.css:
@keyframes breathe { from { transform: scale(1); opacity: .6; } to { transform: scale(1.35); opacity: .16; } }
*/
const steps = ["Create account", "Add your business", "Invite your team", "Connect bank"];
const current = 2; // zero-based
const invites = [
{ email: "asha@kestrel.studio", role: "Admin" },
{ email: "dev@kestrel.studio", role: "Member" },
];
export function OnboardingStepper() {
return (
<section aria-labelledby="setup-title" className="mx-auto grid max-w-[540px] gap-5 rounded-lg border border-border bg-bg p-5 font-sans">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h2 id="setup-title" className="text-[15px] font-medium text-fg">Set up your workspace</h2>
<span className="text-[13px] text-fg-subtle">Step {current + 1} of {steps.length}</span>
</div>
<ol className="grid grid-cols-4">
{steps.map((label, i) => {
const done = i < current, now = i === current;
return (
<li key={label} aria-current={now ? "step" : undefined} className={cn("relative grid justify-items-center gap-2 px-0.5 text-center text-xs leading-tight", now ? "font-medium text-fg" : "text-fg-muted")}>
{i < steps.length - 1 && (
<span aria-hidden className="absolute left-[calc(50%+18px)] right-[calc(-50%+18px)] top-[13px] h-0.5 overflow-hidden rounded bg-surface-2">
{done && <span className="block h-full origin-left bg-primary animate-grow-x motion-reduce:animate-none" style={{ animationDelay: 250 + i * 320 + "ms" }} />}
</span>
)}
<span
className={cn(
"relative z-10 grid size-7 place-items-center rounded-full border-[1.5px] text-xs font-medium",
done && "border-primary bg-primary text-primary-fg",
now && "border-primary bg-bg text-primary",
!done && !now && "border-border-strong bg-bg text-fg-muted",
)}
>
{done ? (
<Check className="size-3.5 animate-pop motion-reduce:animate-none" style={{ animationDelay: i * 320 + "ms" }} aria-hidden />
) : (
i + 1
)}
{now && <span aria-hidden className="absolute -inset-[5px] rounded-full border-2 border-primary animate-[breathe_1.6s_ease-in-out_infinite_alternate] motion-reduce:animate-none" />}
</span>
{label}
</li>
);
})}
</ol>
<div className="grid gap-3 rounded-lg bg-surface p-4 animate-rise [animation-delay:700ms] motion-reduce:animate-none">
<div>
<p className="text-[15px] font-medium text-fg">Invite your team</p>
<p className="text-[13.5px] text-fg-muted">They’ll get an email to join Northwind. You can change roles later.</p>
</div>
{invites.map((inv, i) => (
<div key={inv.email} className="grid grid-cols-[1fr_auto] gap-2 animate-rise motion-reduce:animate-none" style={{ animationDelay: 850 + i * 70 + "ms" }}>
<input defaultValue={inv.email} aria-label={"Teammate " + (i + 1) + " email"} className="h-9 min-w-0 rounded border border-border-strong bg-bg px-3 text-sm text-fg outline-none focus:border-primary focus:ring-2 focus:ring-ring" />
<select defaultValue={inv.role} aria-label={"Teammate " + (i + 1) + " role"} className="h-9 rounded border border-border-strong bg-bg px-2 text-sm text-fg">
<option>Admin</option>
<option>Member</option>
</select>
</div>
))}
<div className="flex flex-wrap items-center justify-between gap-2">
<button type="button" className="inline-flex h-8 items-center gap-1.5 rounded px-2.5 text-[13px] font-medium text-fg hover:bg-surface-2"><Plus className="size-4" />Add another</button>
<div className="flex gap-2">
<button type="button" className="h-9 rounded px-3.5 text-sm font-medium text-fg hover:bg-surface-2">Skip</button>
<button type="button" className="h-9 rounded bg-primary px-3.5 text-sm font-medium text-primary-fg transition hover:bg-primary-hover active:scale-[.97]">Send invites</button>
</div>
</div>
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.