shadimage

Spinner

An inline loading indicator that announces itself: a spinning lucide Loader2 inside a role="status" live region with a screen-reader-only label, so a pending operation is heard as well as seen. Reach for it while fetching data, submitting a form, loading a page or a section, as a Suspense or lazy-route fallback, beside a disabled control, inside a table cell or panel that is still filling in, or anywhere you would otherwise drop a bare "Loading…" string. Common asks it answers: "loading spinner", "react spinner component", "loader component", "busy indicator", "activity indicator", "throbber", "accessible loading state", "aria-live loading announcement", "screen reader loading", "Suspense fallback spinner", "spinning Loader2", "animate-spin loader". The usual hand-rolled version — a bare Loader2 with animate-spin dropped straight into the markup — is invisible to assistive technology: the icon is decorative, so nothing is announced and a screen-reader user waits in silence. Official shadcn/ui now ships a spinner of its own, so choose deliberately rather than by search rank: theirs is the Loader2 icon itself carrying role="status" and the hard-coded English string aria-label="Loading", which is announced but cannot be changed without overriding the attribute, and its registry entry adds class-variance-authority to your package.json. Here the icon is aria-hidden and the announcement comes from a real sr-only text node in a wrapping live region, "Loading" by default; set label to say what is actually loading ("Loading invoices") so the same component announces something useful on every screen, and so the string sits where a translation pipeline can find it rather than inside an aria attribute. Take theirs if you want the icon element itself and nothing around it; take this one if the announcement has to say more than "Loading". It is 1rem square and inherits the current text colour, so it sits correctly inside a button, a link, or a line of muted text with no extra styling, and every span prop (id, style, className, data-*) passes straight through to the wrapper. Pick the sibling that matches the shape: loading-button for a button whose own label swaps to a busy state, progress-ring or gauge when the percentage is known — this is the indeterminate "something is happening" case. Styled with shadcn tokens for light and dark themes; lucide-react is the only dependency.

registry/ui/spinner.tsx

import * as React from "react"
import { Loader2 } from "lucide-react"

import { cn } from "@/lib/utils"

interface SpinnerProps extends React.ComponentPropsWithoutRef<"span"> {
  /** Screen-reader label announced while loading. */
  label?: string
}

export function Spinner({
  label = "Loading",
  className,
  ...props
}: SpinnerProps) {
  return (
    <span
      role="status"
      aria-live="polite"
      className={cn("inline-flex items-center", className)}
      {...props}
    >
      <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
      <span className="sr-only">{label}</span>
    </span>
  )
}
Where it goes
statsdata-tablebanner
Visual style
terminal-monodark-futuristic
Interaction
animated-counter
Use cases
dashboard

DependenciesBundle weight · light

icons
lucide-react