Spinner#
Component <neo-spinner> is a circular loading indicator with two modes. Indeterminate rotates a partial ring during work of unknown duration; determinate draws an arc filled to (value − min) / (max − min), the round counterpart of <neo-progress>. Sized off --neo-spinner-size (defaults to 1em) and inherits currentColor. The ring lives in an open shadow root; customize its ring, track, arc, and sun parts with ::part().
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.
Reference#
Attributes #
| Name | Type | Default | Description |
|---|---|---|---|
value | number | none | Present (and without indeterminate) ⇒ determinate ring. Clamped to [min, max]. |
min | number | 0 | Lower bound of the determinate range. |
max | number | 100 | Upper bound of the determinate range. |
indeterminate | boolean | false | Force the spinning mode even when a value is set. A spinner with neither value nor this attribute is indeterminate by default. |
label | string | none | Accessible name. When set (or in determinate mode) the host reports role="progressbar" with the matching aria-value*; a bare unlabelled indeterminate spinner stays decorative. |
easing | string | none | Animate the arc between value commits. Same shorthand as <neo-progress>'s easing: a bare duration (200ms) pairs with --neo-easing; a full value is used verbatim. Suppressed under reduced motion. |
Parts #
| Name | Description |
|---|---|
::part(ring) | The SVG element holding the track and arc. |
::part(track) | Faint full-circle backing ring. |
::part(arc) | The currentColor stroke that spins or fills to the value. |
::part(sun) | Static eight-ray starburst shown under reduced motion. |
CSS variables #
| Name | Default | Description |
|---|---|---|
--neo-spinner-size | 1em | Outer diameter. |
--neo-spinner-duration | 0.8s | Rotation period in indeterminate mode. |
--neo-spinner-ring-width | 3 | Stroke width in the 24-unit viewBox. |
--neo-spinner-track-color | color-mix(in srgb, currentColor 20%, transparent) | Backing-ring color. |
--neo-spinner-arc-transition | 0s linear | Transition applied to the arc between value commits in determinate mode. |
Examples#
Indeterminate#
A bare <neo-spinner> with no value stays decorative and CSS-driven; pair it with adjacent status text.
Determinate (progress ring)#
A faint track with a currentColor arc filled by value. No text is drawn on the ring; the value goes to aria-valuenow, and a wrapping <neo-tooltip> can surface it visually (see below).
Live progression#
The ring is just an attribute; drive value from anything (SSE, a fetch progress callback, a timer) and the arc sweeps to it. easing tweens the arc between commits (same syntax as <neo-progress>); this one is wired to a 60 ms ticker.
Labelled, with value tooltip#
With a label the host reports role="progressbar" with aria-valuemin / aria-valuemax / aria-valuenow, so it's a real, announced progress indicator, not just a decorative glyph. Tooltip wraps it in a <neo-tooltip> so the value (with Unit) appears on hover or focus, never painted on the ring. Color follows currentColor.
Sizes#
Drive the visual size with the --neo-spinner-size custom property. It defaults to 1em so a bare spinner inherits the surrounding line-height naturally; override it inline (or via a wrapper class) to get any other size without touching the markup.
Inherits parent color#
The spinner's stroke is currentColor, so wrapping it in any element with a color recolours both modes. Theme tokens like var(--accent) work too. Below: an indeterminate spin and three determinate rings at 25%, 66%, 100%, each in a different parent colour.
Under prefers-reduced-motion: reduce, the indeterminate spinner swaps the rotating arc for a static eight-ray sun starburst (the canonical lucide loader glyph). That keeps it visually distinct from a determinate ring sitting at ~75%, which is what the frozen 3/4 arc would look like otherwise. The determinate ring keeps its fill and only drops its commit tween, so values jump rather than sweep. The kit's --neo-spinner-duration token sits on its own (not routed through the global --neo-duration-scale) so the spin isn't accidentally killed when other kit motion is dialed down.
Examples: Datastar#
Morphing during interaction#
A Datastar app can fat-morph the page at any moment, switching between unknown progress and server-reported percentages. Each state re-emits only the bare host. The SVG ring lives in the component's shadow root, so the morph leaves the existing arc in place. The easing transition sweeps between determinate values, while the indeterminate state switches the same arc back to its rotating animation. Autoplay cycles through all five states below with no signals or morph hints.
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.