Spinner
StableAn indeterminate activity indicator for short waits when the system is working and the result is on its way.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/spinner.jsonUsage
import { Spinner } from "@/components/ui/spinner"<Spinner label="Loading comments" />prfct's spinner is twelve tapered strokes that rotate in twelve discrete steps. Stepped motion reads as "working" at a glance, and the tapered strokes still look like a spinner when reduced motion freezes it on a single frame.
Examples
Sizes
The spinner is size-4 (16px) — the icon size used across the system — so it drops into buttons, inputs and menu items without adjustment. Scale it with size-* classes; it's an SVG, so it stays crisp at every size.
Colors
The strokes use currentColor. Inherit the surrounding text color by default, or set one with a text-* class — text-muted-foreground for quiet, secondary work.
In buttons
Prefer the Button's loading prop: it keeps the button's width and focus and sets aria-busy. Compose a spinner yourself only when the label should change while the action runs.
<Button variant="outline" disabled>
<Spinner data-icon="inline-start" aria-hidden="true" label="" />
Generating report…
</Button>Loading a section
For a panel that is fetching data, center a spinner with a short sentence about what's happening. If you know the content's layout, a Skeleton is usually the better choice.
Crunching last month's numbers…
Guidelines
When to use
- While a request triggered by the person is in flight: saving, sending, refreshing.
- For background work tied to a specific control or row: Syncing… next to a file.
When not to use
- For waits under about a second. Delay the spinner by ~300ms so fast responses never show it.
- For long tasks with measurable progress — use Progress.
- For content whose layout you know — use a Skeleton.
One spinner at a time
A page full of spinners says "everything is broken". Show the indicator where the person's attention is — the button they pressed, the row they changed — and keep the rest of the interface usable.
Uploading 3 of 12…
Accessibility
- A standalone spinner renders
role="status"witharia-labelfrom thelabelprop (default Loading, or its translation from LocaleProvider). Give it a specific label — Loading comments beats Loading. - When visible text already describes the wait, put
role="status"on the wrapper and hide the spinner witharia-hidden="true", so the message isn't announced twice. - Inside a button, the spinner is decorative: the button's own label and
aria-busycarry the meaning. - Under
prefers-reduced-motionthe rotation stops on a single frame. The tapered strokes still communicate activity without movement.
API reference
Spinner
Renders an <svg> with data-slot="spinner". Accepts all SVG props.