Theme
On this page (4)
Component Skill — components/spinner/component-skill.md

Native basis

CSS @keyframes animation on an SVG element. No JavaScript required.

Web Platform APIs

@keyframesrole="status"<svg>

Classes

.spinner

Sizes (data-size)

xsHeight: 0.625remsmHeight: 0.875remmdHeight: 1.25remlgHeight: 1.5remxlHeight: 2rem

Accessibility

• Must have role="status" and aria-label="Loading" (or contextual label)

• Screen readers announce the loading state via the ARIA live region

§Default

Standard spinner at default size.

§Sizes

The full five-step scale via data-size - the default is 1rem, between sm and md.

§In Button

A loading button is a busy button: aria-busy="true" plus a .spinner child, in any variant - the spinner takes the button's text color and keeps turning. The last one is disabled as well: button.css deliberately freezes a disabled button's spinner (the dimmed button already says 'pending', a wheel spinning on a control you can't use would say 'working').

§CSS view file

Styles for the spinner component. Uses design tokens for colors, spacing, and radius.

@layer components {
  .spinner {
    width: 1rem;
    height: 1rem;
    animation: spinner-rotate 1s linear infinite;
    color: var(--muted-foreground);
    flex-shrink: 0;
    &[data-size="xs"] {
      width: 0.625rem;
      height: 0.625rem;
    }
    &[data-size="sm"] {
      width: 0.875rem;
      height: 0.875rem;
    }
    &[data-size="md"] {
      width: 1.25rem;
      height: 1.25rem;
    }
    &[data-size="lg"] {
      width: 1.5rem;
      height: 1.5rem;
    }
    &[data-size="xl"] {
      width: 2rem;
      height: 2rem;
    }
  }
  @keyframes spinner-rotate {
    to { transform: rotate(360deg); }
  }
}
/* Accessibility: suppress motion for users who request it (REQUIRED for all
   components - AGENTS.md "Accessibility CSS"). Near-zero duration instead of
   `none` keeps transitionend/animationend (and discrete display flips)
   firing so JS state machines that await them keep working. */
@media (prefers-reduced-motion: reduce) {
  @layer components {
    .spinner,
    .spinner *,
    .spinner::before,
    .spinner::after,
    .spinner *::before,
    .spinner *::after,
    .spinner::backdrop {
      transition-duration: 0.01ms !important;
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
    }
  }
}

Comments, ideas or improvements? Edit this page's source on GitHub