PixelSpinner

Compact loading indicator with surface-aware animation (stepped on pixel, smooth on linear) and tone-driven color.

  • Four sizes (xs/sm/md/lg) and seven tones aligned with token palette
  • Surface-aware: 8-step pixel rotation vs smooth linear sweep
  • Respects prefers-reduced-motion (freezes animation, keeps shape)
  • role=status with sr-only label by default; decorative mode for nested use
  • SSR-safe and tree-shakable
Status
stable
Category
feedback
Since
v1.9.0

API

import { PixelSpinner } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
size'xs' | 'sm' | 'md' | 'lg''md'Box size.
labelstring'Loading'Accessible name.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.
tone'neutral' | 'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink''cyan'Colour.
decorativebooleanfalseWhen true, renders as pure decoration (aria-hidden, no role, no label). Use inside an already-announcing parent (e.g. a <button loading> that declares aria-busy="true") to avoid double announcements. NOTE: when using PixelSpinner as a standalone loading indicator (not decorative), the consumer MUST set aria-busy="true" on the loading container per WAI-ARIA — the spinner alone isn't enough context.

Also takes the native attributes and event handlers of <span> (HTMLAttributes<HTMLSpanElement>).

ref points to <span>.

Accessibility

WCAG target: 2.1 AA

  • status

Default renders role=status with aria-label="Loading" and a visually-hidden label for SR. Pass decorative when the parent already announces busy state (e.g. button with aria-busy) to avoid double announcements. Honors prefers-reduced-motion.

Usage

import { PixelSpinner } from '@pxlkit/ui-kit';

export function Default() {
  return <PixelSpinner />
}

Examples

Default

import { PixelSpinner } from '@pxlkit/ui-kit';

export function Default() {
  return <PixelSpinner />
}

Sizes

import { PixelSpinner } from '@pxlkit/ui-kit';

export function Sizes() {
  return (
    <div className="flex items-center gap-4">
      <PixelSpinner size="xs" />
      <PixelSpinner size="sm" />
      <PixelSpinner size="md" />
      <PixelSpinner size="lg" />
    </div>
  )
}

Tones

import { PixelSpinner } from '@pxlkit/ui-kit';

export function Tones() {
  return (
    <div className="flex items-center gap-4">
      <PixelSpinner tone="neutral" />
      <PixelSpinner tone="green" />
      <PixelSpinner tone="cyan" />
      <PixelSpinner tone="gold" />
      <PixelSpinner tone="red" />
      <PixelSpinner tone="purple" />
      <PixelSpinner tone="pink" />
    </div>
  )
}

Pixel surface

import { PixelSpinner } from '@pxlkit/ui-kit';

export function PixelSurface() {
  return <PixelSpinner surface="pixel" size="lg" tone="cyan" />
}

Decorative (inside aria-busy parent)

import { PixelSpinner } from '@pxlkit/ui-kit';

export function Decorative() {
  return (
    <button type="button" aria-busy="true" className="inline-flex items-center gap-2">
      <PixelSpinner decorative size="sm" />
      <span>Saving…</span>
    </button>
  )
}

Related