Spinner
An animated indicator for an indeterminate loading state.
@garn/ui/spinnerOn this page
Default
The default spinner.
Sizing
Glyph size 14/16/20/24/32px — identity (mirrors the button icon ramp).
Labeled
Pair the spinner with visible status text via aria-labelledby.
Related components
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add spinner- Registry
spinner- Deps
class-variance-authoritylucide-react- Registry deps
utils
Import
import { Spinner } from "@garn/ui/spinner";Props
The component's public props and their types.
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
Plus 489 inherited native <svg> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="spinner"]When to use
Indicate an indeterminate loading or pending state, inline or inside a control.
Reach for something else when
- For determinate progress (use a progress bar) or content placeholders (use a skeleton).
Overview
Spinner indicates an indeterminate loading or pending state — inline, beside a label, or inside a control — when you can't report a percentage. Its size follows the same glyph ramp as button icons so it sits cleanly within text and buttons. Use it for short, unknown-duration waits; for a known fraction use Progress, and to reserve a known content shape use a Skeleton.
Guidelines
Use a spinner when duration is unknown and brief. It says "working" without a number; if you can show real progress, a determinate bar is more reassuring, and for laying out incoming content a Skeleton prevents the jump.
Show it in context and accompany long waits. Put the spinner where the result will appear (in the button, beside the row) rather than blanking the whole screen; past a couple of seconds, add a word of context so users aren't left guessing.
Announce loading and respect motion. Give the loading region an accessible "loading" state: name the Spinner, and set aria-busy on the region whose content is being refreshed. Honor reduced-motion, and resolve the spinner to content, empty, or an error instead of leaving it running forever.
Best practices
- Give it an accessible name describing what's loading via aria-label or aria-labelledby.
- Pair it with visible status text when the context is not already clear.
- Set aria-busy on the loading region when its existing content is being refreshed.
- Match the size to the surrounding control.
- Don't block the whole UI with a bare spinner where a skeleton would preserve layout.
- Don't render both the Spinner and its label as separate live regions.
Content guidelines
- Give it an accessible name that says what's loading (“Loading results”), not just “Loading”.
- For a visible label, point aria-labelledby at the text and keep the label concise.
- The Spinner primitive does not render label text; compose the glyph with a nearby <span> when users benefit from seeing the status.
Troubleshooting
Screen reader announces nothing useful while loading.
Cause. The default aria-label is the generic “Loading”, or visible label text is not connected to the status.
Fix. Pass a contextual aria-label, or use aria-labelledby to point at visible text. For a refreshed region, set aria-busy on the region; rely on Button's loading inside a control.
Accessibility
- Role
status
- The SVG is role=status (a polite live region) with a fallback aria-label of “Loading”.
- Use aria-label for a standalone spinner; use aria-labelledby when visible text names the loading state.
- When composing a visual label, keep one status announcer: name the Spinner from the visible text, or hide the glyph and put role=status on the labeled wrapper.
- Animation is paused under prefers-reduced-motion (library-wide guard).
- Status
- Stable
- Version
v0.5.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history for individual components. This reflects the component's current lifecycle state; a full per-release history is coming.