role status
Spinner draft v0.1.0
Says that something is loading when its length is unknown, as a turning ring with a label.
A spinner is the indeterminate loading indicator: a ring turns while a request, a page or a part of it loads, and a label names what loads, shown or kept for assistive technology. Use it for short waits in a small area (a button, a panel); for a long or measurable wait, a Progress bar; for content whose layout is known, Skeletons. Other systems call it an activity indicator, a loading indicator or a progress circle.
Web implementation
Root .ds-spinner on a <div>. Properties map to attributes: size → data-size
packages/components/src/components/spinner/spinner.css
/*
* Spinner — vanilla CSS reference implementation.
* Spec: packages/components/specs/components/spinner.spec.json
* Tokens: packages/tokens/dist/css/components/spinner.css (generated from packages/tokens/src/components/spinner.tokens.json)
*
* Rules:
* - Only --spinner-* custom properties may appear here. No raw values, no semantic tokens.
* - The ring is a bordered circle whose top segment takes the arc color; it rotates at a multiple
* of the motion duration. Under reduced motion it stops turning and pulses its opacity instead,
* so it still says that something is happening.
* - Size is a data attribute; a spinner has no interaction state.
*/
.ds-spinner {
--_size: var(--spinner-ring-size-medium);
--_border-width: var(--spinner-ring-border-width-medium);
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: var(--spinner-container-gap);
vertical-align: middle;
}
.ds-spinner[data-size="small"] {
--_size: var(--spinner-ring-size-small);
--_border-width: var(--spinner-ring-border-width-small);
}
.ds-spinner[data-size="large"] {
--_size: var(--spinner-ring-size-large);
--_border-width: var(--spinner-ring-border-width-large);
}
.ds-spinner__ring {
box-sizing: border-box;
display: inline-block;
flex: none;
inline-size: var(--_size);
block-size: var(--_size);
border: var(--_border-width) solid var(--spinner-ring-border-color);
border-block-start-color: var(--spinner-ring-color);
border-radius: 50%;
animation: ds-spinner-turn calc(var(--spinner-ring-animation-duration) * 4) linear infinite;
}
.ds-spinner__label {
color: var(--spinner-label-color);
font-family: var(--spinner-label-typography-font-family);
font-size: var(--spinner-label-typography-font-size);
font-weight: var(--spinner-label-typography-font-weight);
line-height: var(--spinner-label-typography-line-height);
letter-spacing: var(--spinner-label-typography-letter-spacing);
}
/* A label that is not shown stays in the accessibility tree. */
.ds-spinner__label[data-hidden] {
position: absolute;
inline-size: var(--spinner-label-size);
block-size: var(--spinner-label-size);
margin: calc(-1 * var(--spinner-label-size));
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@keyframes ds-spinner-turn {
to { rotate: 360deg; }
}
@keyframes ds-spinner-pulse {
from { opacity: 1; }
to { opacity: var(--spinner-ring-opacity); }
}
@media (prefers-reduced-motion: reduce) {
.ds-spinner__ring {
animation: ds-spinner-pulse calc(var(--spinner-ring-animation-duration) * 6) var(--spinner-ring-animation-easing) infinite alternate;
}
}
packages/components/src/components/spinner/spinner.template.mjs
/**
* Spinner — HTML template for the web implementation.
* Pure function of the spec properties (packages/components/specs/components/spinner.spec.json) -> HTML string.
* A status live region holding the ring (decorative) and the label, visible or visually hidden:
* assistive technology announces the label when the spinner appears.
*
* Extra sandbox-only prop: `state` (no state other than default).
*/
import { escapeHtml, escapeAttr } from '../../lib/template-utils.mjs';
export const meta = { id: 'spinner', selector: '.ds-spinner' };
export function render(props = {}) {
const { label = 'Loading', showLabel = false, size = 'medium', state = null } = props;
const attrs = ['class="ds-spinner"', 'role="status"', `data-size="${escapeAttr(size)}"`];
if (state && state !== 'default') attrs.push(`data-state="${escapeAttr(state)}"`);
return `<div ${attrs.join(' ')}><span class="ds-spinner__ring" data-part="ring" aria-hidden="true"></span><span class="ds-spinner__label" data-part="label"${showLabel ? '' : ' data-hidden'}>${escapeHtml(label)}</span></div>`;
}
No other platform implementation recorded yet. Platform token outputs live under Platforms.