v0.5

Spinner

Indeterminate loading indicator for async work in buttons, forms, and inline content.

Description

Spinner is a single-element indeterminate loading indicator. It renders as a <div> with role="status" and a default aria-label="Loading". The arc is drawn entirely in CSS using masks and conic gradients, so there is no SVG payload to ship.

Use it whenever the app is doing work whose duration you can't predict: a save in flight, a network call resolving, a slow form field validating, a page bootstrapping. It composes inside Button via loading, inside InputGroup and Select as the slot icon, and stands alone next to inline status copy like "Saving…".

If you can show actual progress, use Progress so the user can pace their wait. If you're masking a layout shape while content loads (a card placeholder, an avatar slot), use Skeleton. For a static presence dot, use a presence-styled Badge. Spinner is for in-flight motion, not idle state.

Installation

pnpm dlx @create-ui/cli add spinner

Usage

import { Spinner } from "@/components/ui/spinner"
<Spinner />

Examples

Variants

variant sets the spinner's color. Twelve options cover the primary intent, status tones (info, success, warning, danger, away), the neutral set, and an inverse* set designed to read on dark surfaces.

Appearance

appearance picks the visual style. gradient (the default) fades from transparent into currentColor with no background track. solid draws a hard-edged arc over a visible track ring, which reads better at small sizes and on busy backgrounds.

Sizes

Four sizes scale the footprint and stroke width together: xs (10px), sm (13.33px, default), md (16.67px), and lg (20px). Use xs for inline-with-text status, lg for empty states and full-page loaders.

Animation

animation picks the rhythm. spin is a steady linear rotation, pulse (the default) rotates and breathes the arc length on the way around, and tick rotates fast then pauses to feel like discrete steps.

Cap

cap controls the end-cap geometry: sharp for a clean cut, rounded (the default) for soft pill caps. The cap only takes visual effect when appearance="solid"; on gradient the arc fades before reaching its tip, so the cap shape never shows.

Line

line sets how much of the circle the arc covers. short covers ~25% and feels lighter, long (the default) covers ~75% and reads as the more energetic loader silhouette.

Accessibility

Spinner is a passive status indicator. It is not focusable and there are no keyboard interactions to document. Keep the rendered text content brief: the component is announced via its aria-label, not its children.

KeyDescription
-Not focusable by default.

ARIA notes:

  • The component sets role="status" automatically. Browsers and assistive tech treat it as a live region, so changes in surrounding labelled text are announced.
  • A default aria-label="Loading" is set. Override it when the operation deserves a more specific label: <Spinner aria-label="Saving draft" />.
  • Pair the spinner with visible status copy ("Saving…", "Loading workspace") when the user needs to know what's happening. The label alone is brief by design.
  • For decorative spinners where the parent control already announces its loading state (e.g. a Button with loading), add aria-hidden="true" to avoid duplicate announcements.

Styling

Tailwind override: the arc tracks currentColor, so the simplest way to recolor is a text-* utility merged via cn():

<Spinner className="text-emerald-500" />

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="spinner" on the root <div>.
  • data-slot="spinner-track" on the background track ring (rendered only when appearance="solid").
  • data-slot="spinner-arc" on the animated arc.
  • data-slot="spinner-cap-head" and data-slot="spinner-cap-tail" on the pill caps (rendered only when appearance="solid" and cap="rounded").
  • data-variant, data-appearance, data-cap, data-line, data-animation, data-size on the root.

Target a state in CSS:

[data-slot="spinner"][data-variant="danger"] {
  /* ... */
}
  • Progress: use this when progress is determinate and you can show a percentage.
  • Skeleton: use this when masking a layout shape while content loads, not for in-flight actions.
  • Button: render the spinner inside via the loading prop instead of placing one next to the button manually.

API Reference

Spinner

Indeterminate loading indicator. Extends React.ComponentProps<"div">, so any standard div attribute (id, role, aria-*, onClick, etc.) is accepted. The component sets role="status" and a default aria-label="Loading"; override aria-label to describe the specific operation.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component's CVA classes via cn(). Color flows from text-* utilities.
childrenReact.ReactNode-Not rendered as labelled content. The component draws its own arc; avoid passing children.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral" "neutral-static" "neutral-soft" "inverse" "inverse-static" "inverse-soft" "info" "danger" "success" "warning" "away""primary"Semantic color intent applied to the arc. inverse* options are designed for dark backgrounds.
appearance"solid" "gradient""gradient"Visual style. solid draws a hard-edged arc on a track ring; gradient fades into currentColor, no track.
cap"sharp" "rounded""rounded"End-cap geometry. Only takes visual effect when appearance="solid".
line"short" "long""long"Arc length around the circle. short covers ~25%, long covers ~75%.
animation"spin" "pulse" "tick""pulse"Animation rhythm. spin rotates steadily, pulse breathes the arc length, tick rotates then pauses.
size"xs" "sm" "md" "lg""sm"Footprint and stroke width. xs (10px), sm (13.33px), md (16.67px), lg (20px).