# Spinner

A small turning ring that says something is being worked on, for a wait with no known end: alone, beside its words, or inside a busy button.

Block: `smbk-spinner`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `sm`, `md`, `lg` | `sm` | The ring is the icon size of its step: sm sits beside small text and inside small controls, md beside body text, lg alone in a region. |
| `tone` | `default`, `subtle` | `default` | Default draws the ring in the main icon color; subtle in the quieter one, for a wait beside secondary text. |
| `label` | on / off | off | Shows the words beside the ring. Without it they are still read aloud, and they show anyway when the reader asks for reduced motion. |

Parts: `smbk-spinner__ring`, `smbk-spinner__label`

## Markup

```html
<span role="status" class="smbk-spinner smbk-spinner--size-sm smbk-spinner--tone-default">
  <span class="smbk-spinner__ring" aria-hidden="true"></span>
  <span class="smbk-spinner__label">Loading results</span>
</span>
```

## React

```tsx
import { Spinner } from './design-system/components/spinner/spinner.js';

<Spinner size="sm" tone="default">Loading results</Spinner>
```

## Stylesheet

```css
/* smbk-spinner — Spinner. Figma "Spinner" 7110:461: Size × Tone × Reduced motion, 12 variants; the words are content.
 * A ring of two arcs that turns for a wait with no known end, alone, beside words, or inside a button while its
 * action runs.
 * Reduced motion is not an axis: under the reader's setting the ring stands still and the words show, so they
 * carry the meaning when nothing moves. */

.smbk-spinner {
  display: inline-flex;
  align-items: center;
  gap: var(--smbk-space-inline-xs);
  vertical-align: middle;
  color: var(--smbk-color-icon-default);
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-spinner) {
  position: relative;
}

.smbk-spinner--tone-subtle {
  color: var(--smbk-color-icon-subtle);
}

/* The ring: two arcs facing each other, the top and bottom quarters of a circle drawn as the box's top and bottom
   edges in the current color, the side edges clear, turning once per spin duration at an even speed (an eased
   turn would speed up and slow down). The box is the icon size of its step, so the ring stands where an icon of
   that step would and nothing around it moves while it turns. The arcs keep the emphasis border's width at every
   size. A ring written as the notched-circle icon, as before, draws its arcs the same way and no glyph. */
.smbk-spinner__ring {
  box-sizing: border-box;
  flex: none;
  width: var(--smbk-size-icon-md);
  height: var(--smbk-size-icon-md);
  border: var(--smbk-border-width-emphasis) solid transparent;
  border-block-color: currentColor;
  border-radius: var(--smbk-radius-full);
  color: inherit;
  font-size: 0;
  animation: smbk-spinner-turn var(--smbk-motion-spin-duration) linear infinite;
}

.smbk-spinner--size-sm .smbk-spinner__ring {
  width: var(--smbk-size-icon-sm);
  height: var(--smbk-size-icon-sm);
}

.smbk-spinner--size-lg .smbk-spinner__ring {
  width: var(--smbk-size-icon-lg);
  height: var(--smbk-size-icon-lg);
}

@keyframes smbk-spinner-turn {
  to {
    transform: rotate(1turn);
  }
}

.smbk-spinner__label {
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
  white-space: nowrap;
}

/* Without the label flag the words are still in the page, for a screen reader, but not drawn. */
.smbk-spinner:not(.smbk-spinner--label) .smbk-spinner__label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  border: 0;
}

/* Reduced motion: the ring stands still and the words show. */
@media (prefers-reduced-motion: reduce) {
  .smbk-spinner__ring {
    animation: none;
  }

  .smbk-spinner:not(.smbk-spinner--label) .smbk-spinner__label {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inline": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      }
    }
  },
  "color": {
    "icon": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Icons."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Quiet icons."
      }
    },
    "text": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    }
  },
  "size": {
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Icon box, large."
      }
    }
  },
  "border-width": {
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    }
  },
  "radius": {
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    }
  },
  "motion": {
    "spin": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.1000}}"
      }
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

A small turning ring that says something is being worked on, for a wait whose end is not known: a search running, a list loading, a file being sent. It differs from the progress bar, which shows how much of a known amount is done, and from the skeleton, which stands in for content that is about to arrive.

## Anatomy

- The block, an inline row that sits on the line of the text around it.
- The ring part, two arcs facing each other on a circle, turning together. It is decorative: a screen reader never reads it.
- The label part, the words that say what is happening. Always present; drawn beside the ring when the label flag is on, and read aloud either way.

## When to use

- A wait of a second or more with no known end, in a region the reader is looking at: a list that is loading, a panel being refreshed.
- Beside its words, in a row that reports a state: "Uploading", "Checking the address".
- Inside a control that is waiting for its results, such as a search box while it searches.

## When not to use

- Inside a button whose action is running. Use the [button](button.md)'s loading flag, which keeps the button's width and name and says it is busy.
- When the amount of work is known. Use the [progress bar](progress.md), so the reader can see how far along it is.
- When the content's shape is known before it arrives. Use the [skeleton](skeleton.md), so the page does not jump.
- For a wait shorter than about a second. Show nothing; a ring that flashes is noise.

## Behavior

The ring turns at an even speed for as long as the spinner is shown. When the reader has asked their system for reduced motion, the ring stands still and the words show beside it, so the words say what is happening when nothing moves.

A spinner has no end of its own. If the wait passes a few seconds, the page shows what is happening and offers a way out, such as a message with a Try again button. A spinner that never stops, with no way to recover, is not supported.

Two tones: the default ring is the main icon color; the subtle ring is quieter, for a wait beside secondary text. Three sizes match the icon sizes, so a spinner can take the place of an icon of the same step.

## Accessibility
### What it does for you

- The spinner is a status region: a screen reader reads its words when it appears, without moving focus. The ring is hidden from assistive technology.
- Without words, the wrapper says "Loading" and warns in development.
- Under reduced motion the ring stops and the words show.

### What you must do

- Give it its words, even when they are not drawn. Say what is happening, such as "Loading results", not just "Please wait".
- Announce completion or failure once, from the page: replace the spinner with the result, or with a message that says what went wrong.

### How to test

- With a screen reader, the words are read when the spinner appears, and focus does not move.
- Turn on reduced motion in the system: the ring stands still and the words show beside it.
- When the wait ends, the page says how it went, once.

## Usage rules

### Do

- Write the words as what is happening: "Saving changes", "Loading projects".
- Replace the spinner with the result as soon as it is ready.
- Offer a way out when the wait runs long.

### Don't

- Put a spinner inside a button by hand. Use the button's loading flag.
- Show several spinners for one wait.
- Leave a spinner turning with no end and no way to recover.
