EDS 0.x
Components

Loading Spinner

Examples

Default

Tones

Figma Type: Primary, Negative, Neutral (theme inverse surfaces), Static Black, Static White.

neutral flips with color mode (light bar on dark surfaces in light mode; dark bar on light surfaces in dark mode). Static tones stay fixed — demo them on matching panels so they stay readable in both modes.

Sizes

Sm 20px · Md 24px (default) · Lg 28px · Xl 44px — outer box including Figma’s 2px padding.

RTL

Rotation is direction-agnostic; labels follow the document language.

API Reference

LoadingSpinner

PropTypeDefaultDescription
tone"primary" | "negative" | "neutral" | "static-black" | "static-white""primary"Figma Type — bar and track colors
size"sm" | "md" | "lg" | "xl""md"Outer box 20px / 24px / 28px / 44px
labelstring"Loading"Accessible name (aria-label when not decorative)
classNamestringExtra classes
aria-hiddenbooleanDecorative mode (no role="status") — used inside busy buttons

Track and bar colors map to --eds-color-border-*, --eds-color-adaptive-*, and --eds-color-static-* tokens.

Accessibility

WCAG success criteria

CriterionLevelHow EDS helps
4.1.3 Status MessagesAAStandalone spinners expose role="status" with an aria-label so assistive tech can surface progress without moving focus
1.4.3 Contrast (Minimum)AAToken-mapped bar/track colors; choose tone appropriate to the background
2.3.3 Animation from InteractionsAAARespects prefers-reduced-motion by slowing rotation (does not disable — spinner remains recognizable)
2.2.2 Pause, Stop, HideAContinuous animation is decorative/informational; paired with status text; motion reduced under user OS preference
1.1.1 Non-text ContentASVG is aria-hidden; meaning is carried by aria-label / parent context, or omitted when decorative

Why it matters

A loading indicator tells users that work is in progress and the UI has not frozen. Without a proper status announcement, screen-reader users may think nothing happened after an action — especially on slow networks or after form submission. Spinners that spin forever without context create anxiety for everyone; a clear, localized label (“Saving changes…”) reduces cognitive load and builds trust.

When embedded in a button, the spinner must stay decorative so assistive tech hears one coherent “Saving, busy” message from the button — not a duplicate “Loading” from the spinner itself.

Keyboard

LoadingSpinner is not focusable and does not participate in tab order. Keyboard interaction applies to the host control (page region, dialog, button) that owns the loading state.

Key / actionBehavior
⇥ TabSkips the spinner — focus moves to the next focusable element in document order
+ ⇥ TabSame — spinner is not in the tab sequence
↵ Enter / SpaceNo effect on the spinner itself; activates the focused host control if one is present
⎋ EscNo effect on the spinner; may dismiss a parent dialog or cancel a parent action
Host contextWhat keyboard users experience
Standalone page loaderFocus remains on last focused element or moves per app logic; spinner is visual-only unless label announces status
Button with loadingButton stays focusable; aria-busy="true" on the button conveys busy state; spinner is aria-hidden
Modal / drawer overlayFocus trap on the dialog; spinner inside does not steal focus

Screen readers & semantics

ModeDOM semanticsWhat AT announces
Standalone (default)role="status" + aria-label from label prop (default "Loading")Status region — label is read when inserted or when content changes (behavior varies by AT)
Decorativearia-hidden="true" — no role="status", label ignoredNothing — parent must convey loading (aria-busy, visible text, live region)
SVG graphicInner <svg aria-hidden="true" focusable="false">Never announced separately from the status label
Prop / attributeRole
labelSets accessible name via aria-label when not decorative; localize for the action (“Uploading file…”)
aria-label (native attr)Overrides label when provided
aria-hiddenSwitches to decorative mode — required when nested in Button loading or other labeled hosts

The spinner does not use aria-live directly; role="status" maps to an implicit live region in most browsers. Avoid nesting multiple standalone spinners with identical labels on one screen.

Focus & visuals

ConcernEDS behavior
FocusSpinner is not focusable (tabIndex not set); no focus ring on the spinner itself
ContrastBar and track use design tokens (--eds-color-border-*, --eds-color-adaptive-*, --eds-color-static-*); pick tone to meet 3:1 non-text contrast against the surface
MotionContinuous rotation at 0.75s per revolution; under prefers-reduced-motion: reduce, animation slows to 1.5s (still indeterminate, less aggressive)
RTLRotation is direction-agnostic; label text follows document lang / reading direction

neutral tracks theme inverse surfaces. static-black / static-white stay fixed — use them on branded or locked light/dark panels where theme colors must not shift.

Build with it

DoDon’t
Use a specific label for the action: "Saving profile", "Deleting item"Leave the default "Loading" when context is unclear
Set aria-hidden when the parent already announces busy state (Button loading, aria-busy region)Render a standalone spinner and duplicate the same message on the parent
Match tone to the surface (neutral on inverse; static-white / static-black on fixed dark/light panels)Put static-black on a dark surface or static-white on a light one
Remove or hide the spinner when loading completesLeave a spinner visible indefinitely with no status update
Localize label for RTL locales (see Usage → RTL)Hard-code English labels in localized apps

Button loading patternButton sets aria-busy="true" and renders <LoadingSpinner aria-hidden /> so users hear one busy button, not spinner + button:

<Button loading>Saving…</Button>

Standalone page loader:

<LoadingSpinner label="Loading dashboard" size="lg" />

Quick test

  1. Standalone announcement — Render <LoadingSpinner label="Loading data" />. With VoiceOver (macOS) or NVDA (Windows), verify the status label is available in the rotor / element list (role="status").
  2. Decorative mode — Render <LoadingSpinner aria-hidden /> inside a <div aria-busy="true">Loading…</div>. Confirm the spinner itself is not announced separately.
  3. Button loading — Toggle Button loading. Confirm you hear “busy” (or equivalent) once from the button, not a second “Loading” from the spinner.
  4. Reduced motion — Enable Reduce motion in OS settings. Reload — rotation should be noticeably slower (1.5s vs 0.75s).
  5. Contrast spot-check — Place each tone on its intended surface; confirm the moving bar is visible at arm’s length and meets team contrast targets.
  6. RTL — Set dir="rtl" lang="fa" on a parent with a Persian label; confirm label text reads correctly and layout is unaffected.

On this page