Skip to content

Spinner

Stable

An indeterminate activity indicator for short waits when the system is working and the result is on its way.

Syncing 24 files…

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/spinner.json

Usage

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…

Do.The spinner sits next to the thing that's working, with a short label.
Don’t.A spinner with no context leaves people guessing what they're waiting for.

Accessibility

  • A standalone spinner renders role="status" with aria-label from the label prop (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 with aria-hidden="true", so the message isn't announced twice.
  • Inside a button, the spinner is decorative: the button's own label and aria-busy carry the meaning.
  • Under prefers-reduced-motion the 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.

PropTypeDefault
label

Accessible name, applied as aria-label. Defaults to the locale's loading message. Pass an empty string when the spinner is decorative.

stringmessages.loading
className

Size (size-*) and color (text-*). Defaults to size-4 and currentColor.

stringNo default