Skip to contentVibraUI
Feedback & status

Spinner

A turning loader icon, announced as a status.

shadcn's base-nova spinner with one change: it stops turning for a reader who has asked for less motion — by the OS setting or a [data-motion="reduced"] above it — where shadcn's turns regardless. A spin is a loop, so it stops rather than slows. It is lucide's Loader2 with role="status" and the name Loading. Give it a name of its own when it stands alone, saying what it waits on; hide it with aria-hidden when words beside it already say what is loading — a button's label, a status line, a sentence — so the reader is not told twice. Size it with a size class, or in em inside running text.

Install

npx shadcn@latest add @vibra/spinner

Needs the @vibra registry in your components.json — set it up once.

Examples

Sizes

One icon at four sizes, each captioned with the place it belongs, from a badge to a page.

With its own name

Nothing else stands in the panel, so the spinner is a status named for what it waits on, read once.

In a line of text

Sized in em and set on the text's line, at the end of sentences that already say what is happening.

In a button

The label stays, out of sight, so the button keeps its width and its name; aria-busy says it is working.

A row at work

One source syncing in a list at rest: the spinner takes the place of the row's tick, beside words that say what it syncs.

The step at work

A setup that runs on its own: the running step shows the spinner, and every step says its state in words.

Over a card

Refresh keeps the last figures under a veil, busy and inert; the line under the title says what is happening.

Loading more

A spinner row stands where the next orders will appear; then the focus moves to the first of them.

Held still when asked

The app's Reduce motion switch sets data-motion="reduced", which stops the spin as the OS setting does.

Props

PropTypeDefaultDescription
aria-labelstring"Loading"The status's name. When the spinner stands alone, say what it waits on — "Loading revenue by region"; it is read once, as it appears.
aria-hiddenboolean—Set it when the words beside the spinner already say what is loading: inside a button, beside a status line, at the end of a sentence.
classNamestring"size-4"The size and colour: size-3 in a badge, size-4 in a button or a line, size-5 in a panel, size-8 over a page, size-[1em] inside text.

Dependencies

Source

components/ui/spinner.tsx
import { cn } from "@/lib/utils"
import { Loader2Icon } from "lucide-react"

function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
  return (
    // A spin is a loop, so no token can time it: it turns only for a reader
    // who has not asked for less motion — by the OS setting or a
    // [data-motion="reduced"] above it — and the status and its name still
    // say it is working when it is still.
    <Loader2Icon
      data-slot="spinner"
      role="status"
      aria-label="Loading"
      className={cn("size-4 motion-safe:not-in-data-[motion=reduced]:animate-spin", className)}
      {...props}
    />
  )
}

export { Spinner }