Browse the docs
Feedback / Loading

Loading states

Spinners, progress, skeletons and the in-between states nobody designs until it's too late. Drawn by hand, honest about time, quiet for screen readers and still under reduced motion.

Server ComponentsWCAG AANew

Which one?

  • Spinner0.3 to 3 s

    A wait of up to a few seconds, when you can't show the shape of what's coming: sending, saving, a button press.

  • Skeleton1 s and up

    A layout you know the shape of, taking a second or more: lists, cards, profiles. Nothing jumps when it lands.

  • ProgressAny length

    You know how far along it is: uploads, syncs, steps. Switch from indeterminate to a value as soon as you know one.

Under 300 ms, show nothing (useDelayedLoading). Past a few seconds, say what's slow and offer Cancel (Loader). Put aria-busy on the region that's loading, and never spin forever in silence.

Spinners

For waits under a few seconds. Pick one per product; Hum is for moments with personality.

  • Loading (Ink loop)Ink loopThe default.
  • Loading (Hopping dots)Hopping dotsButtons, chat.
  • Loading (Scribble)ScribbleWriting, drawing.
  • Loading (Coin flip)Coin flipThe fun waits.
  • Loading (Hum)HumWhole pages.
LoadingLoadingLoadingLoadingLoadingOn ink
Code
<Spinner label="Loading sketches" />
<Spinner kind="dots" showLabel label="Saving your board" />
<Spinner kind="sticker" tone="bubblegum" size="lg" delay={300} />
<Button loading={isSending} loadingLabel="Sending">Send</Button>

Progress

When you know how long it takes, say so. When you don't, keep it moving and honest.

Syncing reports
Finding the closest unit
Storage used
  1. Drawing the stickers, current step
  2. Printing in one ink, not started
  3. Cutting them out, not started
  4. Packing your board, not started
Code
<Progress label="Syncing reports" value={4} max={10} valueText="4 of 10" />
<Progress label="Finding the closest unit" indeterminate tone="tomato" />
<ProgressRing label="Uploading photos" value={64} />
<StatusTimeline steps={steps} current={1} />

Skeletons

For layouts that take a second or more. The scraps match the real shapes, so nothing jumps when it lands.

Code
<section aria-busy={isLoading} aria-label="Crew">
  {isLoading ? (
    <>
      <Skeleton shape="circle" width={52} height={52} />
      <SkeletonText lines={2} />
    </>
  ) : (
    <CrewList people={people} />
  )}
</section>

Built in

Cards, Select, Avatar and Tabs panels each have a loading prop: the same shape with paper scraps, aria-busy, and no custom CSS.

Loading

Loading

Brush
Loading
Code
<TicketCard loading title="" />
<UnitCard loading name="" />
<Select label="Brush" options={[]} loading />
<Avatar name="Rosa Santos" loading />
<TabPanel value="drafts" loading />

In context

Buttons keep their size, slow loads explain themselves, long jobs show what's happening, and a whole-page wait can always be left.

Buttons
Saving your board
A slow load
An upload
menu.pdf
A whole pageOpens a page loader. Close it, press Cancel or Escape.
Code
<Loader label="Loading the crew" slowAfter={4000} onCancel={stop} />
<Loader variant="card" paper="dots" label="Pressing your page"
  tips={["Checking the ink."]} onClose={close} onCancel={close} />
const showSpinner = useDelayedLoading(isPending);

Change every prop live in the playground.

Props

Spinner
PropTypeDefaultWhat it does
classNamestringnoneExtra class on the wrapper.
delaynumber0Wait this many milliseconds before showing it, so quick loads don't flicker.
kind"dots" | "ink" | "scribble" | "sticker" | "buddy"inkHow it waits. `ink` (default) is a marker loop chasing itself; `dots` hop and squash; `scribble` writes and erases a line of loops; `sticker` flips a die-cut star like a coin; `buddy` is Hum, thinking it over.
labelstring"Loading"What's loading, read out to screen readers.
seedstring | numbera stable per-instance idSeeds the wobble so a spinner looks the same on every render.
showLabelbooleanfalseShow the label next to the spinner instead of only reading it out.
size"sm" | "md" | "lg"md`sm` 20px, `md` 32px, `lg` 52px.
styleCSSPropertiesnoneInline styles on the wrapper, e.g. a `color`.
tone"ink" | "periwinkle" | "mint" | "tomato" | "mustard" | "bubblegum"mustardThe `sticker` kind's fill. The other kinds draw in the color around them.
Progress
PropTypeDefaultWhat it does
label requiredReactNode—What is progressing, e.g. "Syncing offline sketches".
classNamestringnoneExtra class on the wrapper around the label and bar.
indeterminatebooleanfalseFor waits of unknown length: a band of hatching sweeps across instead of filling. No value is read out.
maxnumber100The value that means "done".
solidbooleanfalseA solid bar instead of crayon hatching.
tone"ink" | "mint" | "tomato"inkHatching color: `mint` for good news, `tomato` for running out.
valuenumbernoneHow far along, from 0 to `max`; values outside that range are clamped. Leave it out with `indeterminate`.
valueTextstring"<n>%" (nothing when indeterminate)Shown on the right and read by screen readers, e.g. "4 of 10".
ProgressRing
PropTypeDefaultWhat it does
label requiredstring—What is progressing, read out to screen readers, e.g. "Uploading photos".
value requirednumber—How far along, from 0 to `max`; values outside that range are clamped.
classNamestringnoneExtra class on the ring.
hideValuebooleanfalseHide the number in the middle (it's still read out).
maxnumber100The value that means "done".
seedstring | numbera stable per-instance idSeeds the wobble of the circle.
size"sm" | "md" | "lg"md`sm` 56px, `md` 88px, `lg` 120px.
tone"ink" | "mint" | "tomato"inkInk color of the fill: `mint` for good news, `tomato` for running out.
valueTextstring"<n>%"Read by screen readers and shown in the middle, e.g. "3 of 8".
Skeleton
PropTypeDefaultWhat it does
animatedbooleantrueSweep a pencil-hatch shimmer across it. Off under reduced motion.
classNamestringnoneExtra class on the scrap.
heightstring | numberthe shape's usual sizeHeight as a number of px or any CSS length.
seedstring | numbera stable per-instance idSeeds the hand-cut corners and tilt.
shape"title" | "circle" | "line" | "block" | "pill"none`line` for a line of text (default), `title` for a heading, `circle` for an avatar, `block` for an image or card, `pill` for a tag.
styleCSSPropertiesnoneInline styles on the scrap.
widthstring | numberthe shape's usual sizeWidth as a number of px or any CSS length.
SkeletonText
PropTypeDefaultWhat it does
animatedbooleantrueSweep the shimmer.
classNamestringnoneExtra class on the wrapper.
linesnumber3How many lines.
seedstring | numbera stable per-instance idSeeds the line lengths.
Loader
PropTypeDefaultWhat it does
cancelLabelstring"Cancel"Words on the Cancel button.
classNamestringnoneExtra class on the loader.
closeLabelstring"Close"Accessible name of the close button.
kind"dots" | "ink" | "scribble" | "sticker" | "buddy"ink inline, buddy on a cardThe spinner.
labelstring"Loading"What's loading, e.g. "Loading your sketches". Shown and read out.
onCancel() => voidnoneCalled by the Cancel button. Without it there's no Cancel button.
onClose() => voidnoneCard only: shows a close button in the corner. Use it when someone can leave the wait (Escape should do the same).
paper"kraft" | "dots" | "plain" | "lined"noneCard only: the paper it's printed on. `dots` (default), `lined`, `kraft` or `plain`.
size"sm" | "md" | "lg"mdSpinner size for the inline loader.
slowAfternumber4000After this many milliseconds, say it's slow and offer Cancel. 0 never does.
slowMessageReactNode"This is taking longer than usual."What to say when it's slow.
tape"none" | "periwinkle" | "mint" | "mustard" | "bubblegum"mustardCard only: the tape holding it down.
tipEverynumber2600Card only: milliseconds per tip.
tipsstring[]noneCard only: little lines that rotate while it waits ("Sharpening pencils…"). Not read out on each change.
variant"inline" | "card"none`inline` sits in a row of content (default). `card` is a paper card for a whole page or panel, with Hum and tips.
TabPanel
PropTypeDefaultWhat it does
value requiredstring—Matches the Tab that shows it.
childrenReactNodenone—
classNamestringnoneExtra class on the panel.
loadingbooleanfalseStill on its way: skeleton lines instead of the content, and `aria-busy`. The tabs keep working.
loadingLinesnumber3How many skeleton lines while loading.

Use it well

DoUnder 300 ms show nothing (useDelayedLoading). Up to a few seconds, a spinner. A second or more for a layout, skeletons. When you know how far along it is, Progress. Past a few seconds, say why and offer Cancel.
Don'tSpin forever in silence, or trap someone in a full-page wait with no way out.

Accessibility

  • Spinner is role="status" with its label read out once; every drawing is aria-hidden.
  • Progress and ProgressRing are role="progressbar" with value, min, max and aria-valuetext; indeterminate bars leave the value out.
  • Skeletons are aria-hidden: put aria-busy="true" on the region that's loading and drop it when the content lands.
  • Loader's slow note is its own status, read out when it appears; Cancel and Close are real buttons.
  • Buttons keep their size and focus while loading and set aria-busy.
  • Every loop is a transform or opacity change the compositor runs, so a page full of loaders stays smooth. Under reduced motion drawings hold still and three dots fade.
Five stamps!

All aboard.

You took the whole tour. Here's Holo Pip, a foil sticker for riders who finish. It's in your playground tray now, for good.