Feedback
Spinner
An indeterminate activity indicator for waits with no known length. Takes the colour of its text.
- Built on
<svg>role=statusprefers-reduced-motion- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.1 kB
- Dependencies
- React
Deploying to eu-west-1
Try its options
Loading its options.
Code
import { Spinner } from "@/components/equip/spinner";
export default function Demo() {
return (
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--eq-ink-2)" }}>
<Spinner size="s" label="Deploying to eu-west-1" />
<span aria-hidden="true">Deploying to eu-west-1</span>
</div>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--eq-ink-2)">
<span role="status" class="eq-spinner" data-size="s">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Deploying to eu-west-1</span>
</span>
<span aria-hidden="true">Deploying to eu-west-1</span>
</div>Examples
Sizes
Loading commentsLoading commentsLoading comments
Code
import { Spinner } from "@/components/equip/spinner";
export default function Example() {
return (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<Spinner size="s" label="Loading comments" />
<Spinner size="m" label="Loading comments" />
<Spinner size="l" label="Loading comments" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;gap:16px;align-items:center">
<span role="status" class="eq-spinner" data-size="s">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Loading comments</span>
</span>
<span role="status" class="eq-spinner" data-size="m">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Loading comments</span>
</span>
<span role="status" class="eq-spinner" data-size="l">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Loading comments</span>
</span>
</div>Takes the text colour
The spinner draws in currentColor, so it follows whatever text it sits beside without a tone prop.
Syncing contactsChecking for updatesRetrying payment webhook
Code
import { Spinner } from "@/components/equip/spinner";
export default function Example() {
return (
<div style={{ display: "grid", gap: 10, fontSize: 13 }}>
<span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-ink)" }}>
<Spinner size="s" label="Syncing contacts" />
<span aria-hidden="true">Syncing contacts</span>
</span>
<span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-ink-3)" }}>
<Spinner size="s" label="Checking for updates" />
<span aria-hidden="true">Checking for updates</span>
</span>
<span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--eq-warn)" }}>
<Spinner size="s" label="Retrying payment webhook" />
<span aria-hidden="true">Retrying payment webhook</span>
</span>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:10px;font-size:13px">
<span style="display:flex;align-items:center;gap:8px;color:var(--eq-ink)">
<span role="status" class="eq-spinner" data-size="s">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Syncing contacts</span>
</span>
<span aria-hidden="true">Syncing contacts</span>
</span>
<span style="display:flex;align-items:center;gap:8px;color:var(--eq-ink-3)">
<span role="status" class="eq-spinner" data-size="s">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Checking for updates</span>
</span>
<span aria-hidden="true">Checking for updates</span>
</span>
<span style="display:flex;align-items:center;gap:8px;color:var(--eq-warn)">
<span role="status" class="eq-spinner" data-size="s">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Retrying payment webhook</span>
</span>
<span aria-hidden="true">Retrying payment webhook</span>
</span>
</div>Holding a panel
The panel sets aria-busy while it waits, and the spinner's label says what it is waiting for.
Code
import { Spinner } from "@/components/equip/spinner";
export default function Example() {
return (
<section
aria-busy="true"
aria-label="Build logs"
style={{
display: "grid",
placeItems: "center",
width: "100%",
maxWidth: 280,
height: 96,
border: "1px solid var(--eq-line)",
borderRadius: "var(--eq-radius-l)",
color: "var(--eq-ink-3)",
}}
>
<Spinner size="l" label="Loading build logs" />
</section>
);
}HTMLworks in any framework, with the part's CSS
<section aria-busy="true" aria-label="Build logs" style="display:grid;place-items:center;width:100%;max-width:280px;height:96px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l);color:var(--eq-ink-3)">
<span role="status" class="eq-spinner" data-size="l">
<svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
</svg>
<span class="eq-spinner-label">Loading build logs</span>
</span>
</section>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
size | "s" | "m" | "l" | "m" | 16, 20 or 24px. |
label | string | "Loading" | Read by screen readers and visually hidden. Say what is loading. |
...rest | ComponentProps<"span"> | none | Every native span attribute. |
Anatomy
| root | .eq-spinner | A span with role="status". |
|---|---|---|
| ring | .eq-spinner-ring | The SVG. Hidden from assistive tech. |
| track | .eq-spinner-track | The faint full circle behind the arc. |
| arc | .eq-spinner-arc | |
| label | .eq-spinner-label | Visually hidden text. |
Notes
- The root is role="status", a polite live region; a status that is already on the page when its text appears is announced more reliably than one inserted with its text.
- Make the label specific, such as "Loading invoices", and remove the spinner when the content arrives.
- Colour comes from currentColor, so it matches the text around it and needs no tone prop.
- Under prefers-reduced-motion the turn slows to 2.4 seconds instead of stopping, so it still reads as working.
- For waits with a known length, show progress instead.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/spinner.tsx34 lines
import type { ComponentProps } from "react";
import "./spinner.css";
export type SpinnerProps = ComponentProps<"span"> & {
/** 16, 20 or 24px. */
size?: "s" | "m" | "l";
/** What is happening, read by screen readers. Make it specific: "Loading invoices". */
label?: string;
};
export function Spinner({ size = "m", label = "Loading", className, ...rest }: SpinnerProps) {
return (
<span
role="status"
{...rest}
className={["eq-spinner", className].filter(Boolean).join(" ")}
data-size={size}
>
<svg
className="eq-spinner-ring"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
aria-hidden="true"
>
<circle className="eq-spinner-track" cx="8" cy="8" r="6.25" />
<circle className="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" strokeDasharray="28 100" />
</svg>
<span className="eq-spinner-label">{label}</span>
</span>
);
}components/equip/spinner.css63 lines
@layer equip {
.eq-spinner {
--_size: 20px;
position: relative;
display: inline-flex;
flex: none;
width: var(--_size);
height: var(--_size);
vertical-align: middle;
}
.eq-spinner[data-size="s"] {
--_size: 16px;
}
.eq-spinner[data-size="l"] {
--_size: 24px;
}
.eq-spinner-ring {
display: block;
width: 100%;
height: 100%;
/* Start the arc at twelve o'clock. */
rotate: -90deg;
animation: eq-spinner-spin 700ms linear infinite;
}
.eq-spinner-track {
opacity: 0.2;
}
@keyframes eq-spinner-spin {
from {
rotate: -90deg;
}
to {
rotate: 270deg;
}
}
/* The one motion allowed to run on its own. It slows down rather than stopping,
because a still arc would read as finished. A literal duration, since --eq-dur is 0 here. */
@media (prefers-reduced-motion: reduce) {
.eq-spinner-ring {
animation-duration: 2.4s;
}
}
.eq-spinner-label {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
}