Spinner

Spinner is the inline loading indicator — a slim accent-coloured ring that spins until the work behind it finishes. It shares the same 2xs → 2xl sizing scale as Button, Chip, and Shortcut, so it slots into any control or surface at a matched proportion. Pass it as the icon of a Toast for "Uploading…", drop it inside a Button for an inline "Saving" state, or stick it in an Input suffix for an async-validation indicator.

<Spinner size="2xs" />
<Spinner size="xs" color="brand" />
<Spinner size="sm" color="green" />
<Spinner size="md" color="red" />
<Spinner size="lg" color="purple" />
<Spinner size="xl" color="cyan" />
<Spinner size="2xl" color="yellow" />

Usage

import { Spinner } from '@cladd-ui/react';
 
<Spinner size="lg" color="brand" />;

A bare Spinner is a single self-contained element — no portal, no overlay, no surrounding text. Size and accent color are the only knobs. It picks up the theme accent automatically; pass color to override.

Examples

Sizes

size accepts the standard cladd scale — 2xs, xs, sm (default), md, lg, xl, 2xl. Reach for 2xs / xs for the rare case where the spinner sits inline with very small text, sm and md for typical inline usage, and lg and up for standalone "loading panel" states where the spinner is the main thing on the surface.

When the spinner is nested inside a Button, Input, or other sized control, pass the same size token to both. The size scale is calibrated so that a md spinner is already a touch smaller than a md button — same way a md chip is — so they line up at the right proportion without any extra adjustment. The spinner itself renders at the same dimension whether it's free-standing or nested; the "smaller-than-the-button" feel is baked into the token, not the context. So a md button gets a md spinner, a lg input gets a lg spinner, etc.

Chip is the one exception. A chip already sits at that "one step down from a button" proportion, so dropping a same-size spinner inside it would crowd the label — the convention there is one step smaller (see Inside a chip below).

<Spinner size="lg" color="brand" />

Colors

color accepts any of the eleven cladd accent tokens — the ring takes the accent's primary tone. By default the spinner inherits the theme accent set on CladdProvider, so a brand spinner doesn't usually need a color prop at all. Pass one when you want to signal severity (red for failing work, yellow for a warning state) or match the spinner to a non-default control like a green "deploying" pill.

<Spinner size="xl" color="brand" />

Inside a button

The most common spot for a spinner — a button that needs to communicate "in flight" without changing footprint. Drop <Spinner size={size} /> into the button's children and pass the same size token to both, and the spinner sits at the right proportion for the button's content row. Pair it with readOnly on the button to prevent double-submits while still keeping the button at full opacity (disabled works too but reads as "unavailable" rather than "busy").

<Button size="md" color="brand" readOnly>
  {loading ? <Spinner size="md" /> : null}
  {loading ? 'Saving' : 'Save changes'}
</Button>
<Button size="md" variant="solid" readOnly>
  {loading ? 'Loading' : 'Refresh'}
  {loading ? <Spinner size="md" /> : null}
</Button>

Inside a chip

Chip accepts any component in its icon slot — including Spinner. The convention here is one step smaller than the chip: a md chip pairs with a sm spinner, a lg chip with a md spinner, and so on. The chip already reserves a slot for a glyph, and the spinner reads as that glyph rather than a co-equal element — sizing it down keeps the ring from crowding the label. Forward the chip's color through iconProps so the ring tone matches the rest of the chip.

Useful for "deploying", "indexing", or any other ambient background-job indicator that wants a tag-shaped pill instead of a full button.

Deploying
Indexing
<Chip
  size="md"
  color="brand"
  outline
  icon={Spinner}
  iconProps={{ size: spinnerSize, color: 'brand' }}
>
  Deploying
</Chip>
<Chip
  size="md"
  color="yellow"
  icon={Spinner}
  iconProps={{ size: spinnerSize, color: 'yellow' }}
>
  Indexing
</Chip>

Inside an input

Input exposes prefix / suffix slots that accept any ReactNode — drop a Spinner into suffix while an async check is in flight (username availability, search debounce, autosave). Stick to xs or 2xs here so the indicator stays out of the way of the text.

@
<Input
  size="md"
  className="w-72"
  value={value}
  onChange={(next) => setValue(next)}
  placeholder="Pick a username"
  prefix={<span className="ml-2 text-cladd-fg-softer">@</span>}
  suffix={checking ? <Spinner size="md" className="mr-2" /> : null}
/>

Inside a toast

Toast's icon prop takes an ElementType — pass Spinner for a "still working" notification. Pair it with timeout={0} so the toast doesn't auto-dismiss while the job is running, then drive it closed from your code when the work completes. Forward color through iconProps to keep the spinner's ring tone aligned with the toast's accent.

The same trick works with the imperative useToast hook — pass icon: Spinner and iconProps: { color: 'brand' } in the options bag.

<ToastRoot>
  <ToastTrigger>
    <Button>Upload file</Button>
  </ToastTrigger>
  <Toast
    icon={Spinner}
    iconProps={{ color: 'brand' }}
    title="Uploading hero.png"
    text="This shouldn’t take long."
    timeout={0}
  />
</ToastRoot>
<Button
  color="brand"
  variant="gradient"
  onClick={() =>
    toast({
      icon: Spinner,
      iconProps: { color: 'brand' },
      title: 'Syncing workspace',
      text: 'Pulling the latest from origin/main.',
      timeout: 0,
    })
  }
>
  Sync (useToast)
</Button>

Standalone loading surface

For full-panel loading states — a tab that hasn't finished fetching, a settings sheet waiting on server data — center a large Spinner inside a Surface with a quiet caption underneath. xl and 2xl are the right sizes here; below lg the spinner reads as decoration rather than the focal point.

Fetching data…
<Surface
  outline
  className="w-72 rounded-2xl"
  contentClassName="flex flex-col items-center justify-center gap-4 p-8"
>
  <Spinner size="xl" color="brand" />
  <span className="text-cladd-fg-soft">Fetching data…</span>
</Surface>

Playground

size and color are the only two knobs — but they compose with every accent in the theme, so a quick scroll through both axes is the fastest way to find the right pairing for your context.

<Spinner size="xl" color="brand" />

API Reference

className: stringExtra classes for the spinner root element.
color: ColorAccent color for the spinning ring. Default: theme accent.
size: SpinnerSize'sm'Spinner dimension. Default 'sm'.