Spinner
A spinner shows that something is happening when you can't say how long it will take.
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export default function SpinnerDemo() {
return (
<>
<Spinner />
<Spinner className="size-6" />
<Spinner className="size-8" />
<Button>
<Spinner aria-hidden />
Saving
</Button>
<Button variant="secondary">
<Spinner aria-hidden />
Loading
</Button>
</>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { LoaderCircle } from "lucide-react";
import { cx } from "@/lib/cx";
// A spinning ring for something that is happening for an unknown time. It draws in the current
// text color, is 16px unless you size it, and stops spinning for reduced motion. It announces
// "Loading"; inside a button that already says what is happening, pass aria-hidden.
export function Spinner({ className, ...props }: ComponentProps<typeof LoaderCircle>) {
return (
<LoaderCircle
role="status"
aria-label="Loading"
className={cx("size-4 shrink-0 motion-safe:animate-spin", className)}
{...props}
/>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
className | string | — | Sizes the spinner: size-6 or size-8. It is 16px by default. |
aria-hidden | boolean | — | Hides it from assistive tech. Use it inside a button that already says what is happening. |
Spinner is a lucide icon, so it takes every normal svg prop.
The thinking
A spinner is the honest answer to how long will this take when you don't know. When you do know, show a measure instead.
- Unknown duration only: Use Progress when you can say how far along something is, and a Skeleton when you know the shape of what is coming.
- It draws in the current color: It takes the text color of whatever it sits in, so it matches inside a green button, an outline button or plain text.
- Pair it with a label: In a button, show the spinner and say what is happening, like Saving. Disable the button while it works so it can't be sent twice.
Interface design
- Size: 16px by default, with a shrink guard so it stays round in a tight row. Size it with a class.
- Motion: One full turn every second, at a steady speed.
- In a button: It sits before the label, 8px away, like any icon in a Button.
Interaction
- Not interactive: A spinner has no hover, focus or pressed state.
- Reduced motion: For people who ask their system for less motion it stops turning. The label next to it, such as Saving, still says what is happening.
Empathy
- Announced as loading: On its own it has a status role and the name Loading, so a screen reader mentions it politely.
- Don't announce it twice: Inside a button that already has text, pass
aria-hidden, as the demo does, so the screen reader reads Saving and not Loading, Saving. - Time matters: If something can take more than a few seconds, say so in words as well as showing a spinner.