Spinner
The wait, drawn. A circle whose arc turns until whatever is loading has loaded.
Overview
The wait, drawn. A circle whose arc turns until whatever is loading has loaded.
First example
<Spinner /><Spinner variant="danger" size="sm" /><Spinner variant="default" accessibilityLabel="Chargement des projets" />
Anatomy
<Spinner />
Two rings and no slots. The root is the track — the full circle in the variant's ink, at a fraction of its opacity — and its one child is the arc that turns over it: the same circle with a quarter missing. The two are one figure rather than two parts, so there is nothing between them for a slot to name.
Usage
<Spinner />
<Spinner variant="danger" size="sm" />
<Spinner variant="default" accessibilityLabel="Chargement des projets" />
Beside a label, in a row:
<Row gap={8} alignItems="center">
<Spinner size="xs" variant="tertiary" />
<Typography variant="body-sm">Synchronisation…</Typography>
</Row>
Frozen, for a screenshot or a test:
<Spinner animation={false} />
Root props
SpinnerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | SpinnerVariant | undefined | No | — |
| size | Size | undefined | No | The diameter, and the thickness that goes with it. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7). There is one thing to colour on a spinner, so it lands on the ring — and on the track behind it, at the same fraction the variant's own ink is faded to. |
| animation | boolean | undefined | No | `false` stops the rotation and mounts no worklet. The ring stays, so a list of rows switched off with `animation={false}` does not change height. A boolean rather than the `AnimationProp` union `PressableFeedback` takes: there is one animation here, so there is nothing for an object to switch off one at a time. |
| style | StyleProp<ViewStyle> | No | — |
Slots
Variants, sizes and colour
Accessibility
accessibilityRole="progressbar" and accessibilityState={{ busy }} are set, and both
stay overridable (R9). There is no default accessibilityLabel: only the caller knows what
is loading, and "loading" alone is what the role already announces.
Migration from legacy
| Legacy | v1 |
|---|---|
<Indicator /> | <Spinner /> |
<Indicator themeColor="red" /> | <Spinner variant="danger" /> |
<Indicator size={32} /> | <Spinner size="lg" /> |
Implementation notes
Notes
No asChild
The one root in the library without one (R12). Slot merges a root's props into a single
element, and that element keeps its own children — so a caller's element could become the
track, but the arc is a second node it has no way to receive. Handing over the track alone
would render a ring that never turns, silently.
Styling is the escape hatch instead: every ViewStyle key is a prop (R14), and style is
still the last word.
Why borders and not an SVG stroke
The reference implementation draws one arc fading from opaque to 55%, which needs a linearGradient and
therefore react-native-svg. That package is an optional peer here, and a component in
the fifteen-component core cannot require one. Two circles of a single ink at two opacities
read as the same figure, cost two views, and pull in nothing.
Button.Spinner is not this component with props
The button's spinner takes its diameter and its colour from the button's own recipe, which
has already resolved both. Handing them to <Spinner size={…} color={…} /> would be R6 in
reverse — a vocabulary prop carrying a computed number. What the two share is the turn, and
that lives in useRotation.