Default
Overview
The Spinner ships from @flatpay-dk/ui as a single SVG arc that rotates in currentColor. One geometry, five sizes — the spinner is the indeterminate twin of the progress bar. Reach for it whenever the system can’t quote a percentage; reach for Progress bar when it can.
Reach for it sparingly
A spinner is an apology. Every time one appears, the user is waiting on us. Optimistic UI, skeleton states, and instant local updates beat a spinner — the spinner is what’s left when they’re not options.
Sizes
Five sizes, picked to align with the type and component scale. The stroke weight scales with the size so the arc reads consistently weighty at every step — thinner doesn’t mean wirier.
size="xs"12 pxInline beside small body text.
Loading xssize="sm"16 pxDefault for inline status, button labels.
Loading smsize="md"20 pxStand-alone in dense rows or cells.
Loading mdsize="lg"32 pxCard-level loading skeletons.
Loading lgsize="xl"48 pxWhole-surface spinners — page or modal load.
Loading xl
Colour inheritance
The spinner inherits from currentColor, so it flips with whatever foreground the surrounding container sets. No colour prop, no theme switch — set the parent text colour and the spinner follows.
bg-card · charcoalbg-foreground · off-whitePatterns
Three places the spinner shows up most. Each one has a different accessibility story — picking the right pattern picks the right announcement.
In a button
The Button component handles this for you — pass loading and the label is swapped for a spinner without reflow. The button itself carries aria-busy; the spinner stays decorative.
Click any button — they all hold the spinner for 1.5 s.
Inline beside a label
Stand-alone, beside text that names what’s happening. Pass label={null} on the spinner so the surrounding sentence does the announcing.
Syncing manifest…
Building preview deployment
Authorising payment
Whole-surface load
For a section or page that’s waiting on its first render. Centre the spinner with a single line of context underneath; never blank the surface entirely.
Loading prototypes…
Motion
One full rotation per second, linear easing — the standard indeterminate cadence. Under prefers-reduced-motion, rotation slows to four seconds rather than stopping. A still spinner reads as broken; a slow spinner reads as patient. Per the Motion foundation, this is the one place transforms aren’t cut entirely under reduced motion — they’re calmed instead.
Accessibility
- Default label.The spinner ships with a visually-hidden “Loading” announced via
role="status"+ aria-live=“polite”. Override with thelabelprop when you can name the operation more specifically — “Saving draft”, “Authorising payment”. - Decorative mode. Pass
label={null}when the spinner sits alongside text or inside a control that already announces the busy state. Two announcements is one too many. - Reduced motion.Honoured automatically — you don’t need to gate it in product code.
- No infinite spinners.If a spin lasts longer than ~10 s, swap to a progress message that names what’s taking so long, or surface a retry. A spinner that never resolves is the second-worst loading experience after a frozen page.
Code
tsx
import { Spinner } from "@flatpay-dk/ui";
// Default — md size, "Loading" SR label
<Spinner />
// Sized
<Spinner size="xs" />
<Spinner size="xl" />
// Custom screen-reader label
<Spinner label="Saving draft" />
// Decorative — when the surrounding text already names the operation
<p className="flex items-center gap-2 text-sm">
<Spinner size="xs" label={null} />
Syncing manifest…
</p>
// Inside a Button — pass `loading` to the Button itself; it draws the spinner for you
<Button loading={isSaving}>Save draft</Button>
// On a coloured surface — currentColor follows the parent
<div className="bg-foreground text-background p-6">
<Spinner size="lg" />
</div>Best practices
- Indeterminate only. When you know what percentage is done, use Progress bar. The spinner is the “we don’t know yet” affordance.
- Skip it under 200 ms.A spinner that flashes for less time than a heartbeat reads as a glitch. If the operation usually finishes that fast, don’t mount the spinner; just block the UI until it settles.
- Pair with context.“Loading” is fine on a known surface; on a slower or rarer operation, name the verb — “Authorising payment”, “Pulling manifest from GitHub”.
- One spinner per surface.A page with three spinners isn’t loading harder; it’s loading louder. Either consolidate to one spinner over the whole section, or replace with skeleton states that hint at the shape coming in.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | 12 / 16 / 20 / 32 / 48 px. Stroke weight scales with size so the arc reads consistently weighty. |
| label | string | null | "Loading" | Visually-hidden screen-reader label. Pass null to mark the spinner decorative when the surrounding context already announces the busy state. |
| className | string | — | Forwarded to the wrapping <span>. Useful for layout (flex / margin) — colour rides on currentColor. |