Light

Spinner

An animated loading indicator to signal that content is being loaded.

Spec · from metadata

When to use

  • Indicating loading state for async operations
  • Inline loading indicator in buttons or forms
  • Small loading placeholders within components

When not to use

  • Full-page loading — use Skeleton components instead
  • Content placeholder — use Skeleton instead
  • Progress with known completion — use Progress component

Anti-patterns

Avoid<Loader2Icon className="animate-spin" />
Prefer<Spinner />

Spinner includes accessibility attributes (role='status', aria-label) that raw icons lack.

Avoid<Spinner aria-label="Loading" role="status" />
Prefer<Spinner />

Spinner already has role='status' and aria-label='Loading' built in.

Accessibility

  • Required ARIAaria-label='Loading' (set automatically)
  • Screen readerRenders as SVG with role='status' and aria-label='Loading'. Screen readers announce the loading state.

Token bindings

TokenCategoryUsage
current (currentColor)colorInherits text color from parent

Import

import { Spinner } from "@timelycare/helix-ui"

Props

interface SpinnerProps extends React.ComponentProps<"svg"> {
  className?: string
}

Styling

PropertyValueTailwind
Size16pxsize-4
AnimationRotateanimate-spin
ColorInherits from parentcurrentColor

Typography Context

  • Font: Adelle Sans (inherits parent context)
  • When used with text (e.g., inside a Button), spacing is handled by the parent's gap utility

Structure

The Spinner renders an SVG icon (default: LoaderIcon from Lucide) with animate-spin. It uses role="status" and aria-label="Loading" for accessibility.

<LoaderIcon
  role="status"
  aria-label="Loading"
  className="size-4 animate-spin"
/>

Common Patterns

Standalone

<Spinner />

Inside a Button

<Button disabled>
  <Spinner data-icon="inline-start" />
  Loading...
</Button>

Multiple Variants

<Button disabled size="sm">
  <Spinner data-icon="inline-start" />
  Loading...
</Button>
<Button variant="outline" disabled size="sm">
  <Spinner data-icon="inline-start" />
  Please wait
</Button>
<Button variant="secondary" disabled size="sm">
  <Spinner data-icon="inline-start" />
  Processing
</Button>

Inline Loading

<div className="flex items-center gap-2 text-muted-foreground">
  <Spinner />
  <span className="text-sm">Loading...</span>
</div>

Full Page Loading

<div className="flex items-center justify-center min-h-[400px]">
  <div className="flex flex-col items-center gap-4">
    <Spinner className="size-8 text-primary" />
    <p className="text-sm text-muted-foreground">Loading your data...</p>
  </div>
</div>

Custom Spinner

You can replace the default icon with any SVG that supports animate-spin:

import { LoaderIcon } from "lucide-react"
import { cn } from "@/lib/utils"

function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
  return (
    <LoaderIcon
      role="status"
      aria-label="Loading"
      className={cn("size-4 animate-spin", className)}
      {...props}
    />
  )
}

Accessibility

  • Uses role="status" to announce loading to assistive technology
  • Includes aria-label="Loading" for screen readers
  • When inside a button, pair with disabled to prevent interaction during loading
  • Add aria-busy="true" on the parent container if content is being replaced

Gotchas

ProblemSolution
Spinner not visibleEnsure parent has a contrasting text color; Spinner inherits currentColor
Button still clickable while loadingAlways pair Spinner with disabled on the Button
Spinner too large/smallOverride with className="size-3" or className="size-5"
Animation jankyEnsure no conflicting transforms on the parent element

See Also


Last updated: February 19, 2026