All blocks
Layouts & Navigationbeginner
Circular Stepper
An SVG progress ring around the current step number.
Installation
Add it with the shadcn CLI, open it in v0, or read the source.
$ npx shadcn@latest add https://stepperize.com/r/circular.jsonDependencies
- @stepperize/react
Requirements
- React 18 or later
- Tailwind CSS
Source
import { defineStepper } from "@stepperize/react";
import { useState } from "react";
const { Stepper } = defineStepper([
{ id: "profile", title: "Profile", description: "Basic information" },
{ id: "workspace", title: "Workspace", description: "Name and logo" },
{ id: "invite", title: "Invite", description: "Add teammates" },
{ id: "finish", title: "All done", description: "Launch your space" },
]);
const RADIUS = 20;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
export function CircularBlock() {
const [launched, setLaunched] = useState(false);
return (
<Stepper.Root
linear
className="w-full max-w-md rounded-xl border bg-background p-6 shadow-sm"
>
{({ stepper }) => {
const progress = (stepper.index + 1) / stepper.count;
const next = stepper.steps[stepper.index + 1];
return (
<>
<div className="flex items-center gap-4">
<div className="relative grid size-16 shrink-0 place-items-center">
<svg
viewBox="0 0 48 48"
className="size-16 -rotate-90"
aria-hidden="true"
>
<circle
cx="24"
cy="24"
r={RADIUS}
fill="none"
strokeWidth="4"
className="stroke-muted"
/>
<circle
cx="24"
cy="24"
r={RADIUS}
fill="none"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={CIRCUMFERENCE * (1 - progress)}
className="stroke-primary transition-all duration-500"
/>
</svg>
<span className="absolute text-sm font-semibold">
{stepper.index + 1}/{stepper.count}
</span>
</div>
<Stepper.Content step={stepper.current.id}>
<h3 className="text-base font-semibold">
{launched ? "Workspace launched" : stepper.current.title}
</h3>
<p className="text-sm text-muted-foreground">
{launched
? "Your space is live and ready for collaborators."
: stepper.current.description}
</p>
{next && !launched && (
<p className="mt-1 text-xs text-muted-foreground/70">
Next: {next.title}
</p>
)}
</Stepper.Content>
</div>
<Stepper.Actions className="mt-6 flex justify-between">
<Stepper.Prev className="inline-flex h-9 items-center rounded-lg border bg-background px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50">
Back
</Stepper.Prev>
{launched ? (
<button
type="button"
onClick={() => {
setLaunched(false);
stepper.reset();
}}
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Restart flow
</button>
) : stepper.isLast ? (
<button
type="button"
onClick={() => setLaunched(true)}
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Launch workspace
</button>
) : (
<Stepper.Next className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
Continue
</Stepper.Next>
)}
</Stepper.Actions>
</>
);
}}
</Stepper.Root>
);
}
When to use it
Good for dashboards or compact panels where a circular progress indicator reads better than a full-width bar.
Accessibility
The SVG ring is `aria-hidden`; the numeric step count beside it carries the meaning for assistive tech.
Customization
Tune the ring radius/stroke and derive `strokeDashoffset` from `stepper.progress`. Recolor with the primary token.