Indeterminate wait indicator — one recipe: a border.medium ring in currentColor, open at the top, turning at 3× motion.duration.slow. It has no color of its own and sits on the icon scale, so it drops into text rows, badges, and controls without new bindings.
One sm/md/lg dialect on the shared icon scale; the default is medium.
| Example | Size | Modifier | Box | Usage |
|---|---|---|---|---|
| Small | --sm | size.icon.sm | Inline with text, dense rows | |
| Medium | ds-spinner | size.icon.md | The default — panels and regions | |
| Large | --lg | size.icon.lg | Feature areas, empty states |
currentColor is the whole color contract: the spinner takes the text color of wherever it sits. What changes per context is the wiring — whether the spinner announces itself or defers to visible text.
| Example | Context | Wiring | Inherits | Usage |
|---|---|---|---|---|
| Standalone | role="status"aria-label | text.primary | A region awaiting content announces itself | |
| Loading preview | Beside text | aria-hidden | text.primary | The text is the announcement — one voice, not two |
| In a badge | aria-hidden | status.info.fg | The badge's fg role colors the ring | |
| In a button | aria-busy | action.secondary.text | The button ships its own busy spinner — never hand-place one |
A spinner says "wait, duration unknown" — and only that.
| Verdict | Rule |
|---|---|
| Do | Announce a standalone spinner with role="status" plus aria-label; beside visible text it goes aria-hidden — the text carries the update. |
| Do | Set aria-busy="true" on a busy button and let it bring its own spinner — a hand-placed one loses the padding, icon, and announcement rules Button already contracts. |
| Don’t | Use a spinner for measurable progress — that is Progress; a spinner only says wait. |
| Don’t | Color a spinner directly — recolor its context and currentColor follows. |