# Spinner

> A turning ring beside the control a person used, while their request runs, with a required label for screen readers.

- Name: `spinner`
- Status: beta
- Categories: feedback
- File: the install writes `spinner.tsx` to your `ui` alias, `@/components/ui` by default.
- Gallery: https://arbokui.com/components/spinner
- Registry item: https://arbokui.com/r/spinner.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/spinner
   ```

## Use it for

- A spinner only marks a wait the person started, at the control they used. It never stands in for content. Content waits with Skeleton.
- Put it beside the control that started the wait, like a search field fetching results or a switch saving its new value.
- Give it a label that says what is happening, like "Searching orders". TypeScript requires one.
- It takes the text color around it. Set the color with a text token, like className='text-muted-foreground'.
- Use size to match what is beside it: sm beside text-sm or a small control, md beside a medium control, lg beside a large one.

## Avoid it for

- A button that is saving. Use loading on the Button. It keeps the width and blocks repeat clicks.
- Content that is loading, like a list, a panel, or a page, even when you do not know its shape. Use Skeleton in its place.
- Progress you can measure. Show how far along it is instead.
- Waits that finish in well under a second. The spinner only flashes. Show nothing until the result arrives.
- Several spinners at once for one wait. Use one, at the control the person used.

## Props

- `label`: `string`. Required. What is happening, in words, like "Searching orders". Rendered as visually hidden text inside the status.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Diameter: half, three quarters, or all of a small control's height. Density scales it.
- `className`: `string`. Extra classes. Later classes win, so className='size-12' replaces the size and 'text-primary' sets the color.
- `children`: `never`. Not allowed. The label is the content.

## Accessibility

- Renders a \<span role="status">, a polite live region. The label is its text, in a visually hidden span.
- TypeScript requires label, so a spinner always has words. A live region is announced by its text, so the label is text, not aria-label.
- Some screen readers miss a status region that arrives together with its text, and some hold back changes inside a region marked aria-busy. When people must hear that loading started, keep one role="status" element on the page, outside any busy region, and change its text, as the Skeleton docs show.
- The ring is an aria-hidden \<svg>. It is not focusable and adds no tab stop.
- It turns once per 0.4 × --motion-pulse-duration. The theme sets that to 0s for motion off and for reduced motion, so the spinner stays still then. A theme without the variable also stays still.
- The arc is currentColor, and the track is the same color at 25%. Put it on text the theme already checks, like muted-foreground, which is held to 4.5:1 on the page.

## Examples

### A spinner with a label

```tsx
<Spinner label="Searching orders" />
```

### Beside a search field, with Skeleton for the results

```tsx
function OrderSearch({ results }: { results: string[] | null }) {
  return (
    <div className="grid gap-3">
      <div className="flex items-center gap-2">
        <Input type="search" aria-label="Search orders" />
        {results === null && <Spinner label="Searching orders" className="text-muted-foreground" />}
      </div>
      {results === null ? (
        <Skeleton className="h-20 w-full" />
      ) : (
        <ul>
          {results.map((result) => (
            <li key={result}>{result}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

### Beside a switch that saves

```tsx
<div className="flex items-center gap-2">
  <Switch defaultChecked>Auto-save</Switch>
  <Spinner label="Saving" className="text-muted-foreground" />
</div>
```
