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

PropTypeDefaultWhat it does
classNamestring—Sizes the spinner: size-6 or size-8. It is 16px by default.
aria-hiddenboolean—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.