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
- Drawing the stickers, current step
- Printing in one ink, not started
- Cutting them out, not started
- 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
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.
ButtonsSaving 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| className | string | none | Extra class on the wrapper. |
| delay | number | 0 | Wait this many milliseconds before showing it, so quick loads don't flicker. |
| kind | "dots" | "ink" | "scribble" | "sticker" | "buddy" | ink | How 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. |
| label | string | "Loading" | What's loading, read out to screen readers. |
| seed | string | number | a stable per-instance id | Seeds the wobble so a spinner looks the same on every render. |
| showLabel | boolean | false | Show the label next to the spinner instead of only reading it out. |
| size | "sm" | "md" | "lg" | md | `sm` 20px, `md` 32px, `lg` 52px. |
| style | CSSProperties | none | Inline styles on the wrapper, e.g. a `color`. |
| tone | "ink" | "periwinkle" | "mint" | "tomato" | "mustard" | "bubblegum" | mustard | The `sticker` kind's fill. The other kinds draw in the color around them. |
Progress
| Prop | Type | Default | What it does |
|---|---|---|---|
| label required | ReactNode | — | What is progressing, e.g. "Syncing offline sketches". |
| className | string | none | Extra class on the wrapper around the label and bar. |
| indeterminate | boolean | false | For waits of unknown length: a band of hatching sweeps across instead of filling. No value is read out. |
| max | number | 100 | The value that means "done". |
| solid | boolean | false | A solid bar instead of crayon hatching. |
| tone | "ink" | "mint" | "tomato" | ink | Hatching color: `mint` for good news, `tomato` for running out. |
| value | number | none | How far along, from 0 to `max`; values outside that range are clamped. Leave it out with `indeterminate`. |
| valueText | string | "<n>%" (nothing when indeterminate) | Shown on the right and read by screen readers, e.g. "4 of 10". |
ProgressRing
| Prop | Type | Default | What it does |
|---|---|---|---|
| label required | string | — | What is progressing, read out to screen readers, e.g. "Uploading photos". |
| value required | number | — | How far along, from 0 to `max`; values outside that range are clamped. |
| className | string | none | Extra class on the ring. |
| hideValue | boolean | false | Hide the number in the middle (it's still read out). |
| max | number | 100 | The value that means "done". |
| seed | string | number | a stable per-instance id | Seeds the wobble of the circle. |
| size | "sm" | "md" | "lg" | md | `sm` 56px, `md` 88px, `lg` 120px. |
| tone | "ink" | "mint" | "tomato" | ink | Ink color of the fill: `mint` for good news, `tomato` for running out. |
| valueText | string | "<n>%" | Read by screen readers and shown in the middle, e.g. "3 of 8". |
Skeleton
| Prop | Type | Default | What it does |
|---|---|---|---|
| animated | boolean | true | Sweep a pencil-hatch shimmer across it. Off under reduced motion. |
| className | string | none | Extra class on the scrap. |
| height | string | number | the shape's usual size | Height as a number of px or any CSS length. |
| seed | string | number | a stable per-instance id | Seeds 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. |
| style | CSSProperties | none | Inline styles on the scrap. |
| width | string | number | the shape's usual size | Width as a number of px or any CSS length. |
SkeletonText
| Prop | Type | Default | What it does |
|---|---|---|---|
| animated | boolean | true | Sweep the shimmer. |
| className | string | none | Extra class on the wrapper. |
| lines | number | 3 | How many lines. |
| seed | string | number | a stable per-instance id | Seeds the line lengths. |
Loader
| Prop | Type | Default | What it does |
|---|---|---|---|
| cancelLabel | string | "Cancel" | Words on the Cancel button. |
| className | string | none | Extra class on the loader. |
| closeLabel | string | "Close" | Accessible name of the close button. |
| kind | "dots" | "ink" | "scribble" | "sticker" | "buddy" | ink inline, buddy on a card | The spinner. |
| label | string | "Loading" | What's loading, e.g. "Loading your sketches". Shown and read out. |
| onCancel | () => void | none | Called by the Cancel button. Without it there's no Cancel button. |
| onClose | () => void | none | Card 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" | none | Card only: the paper it's printed on. `dots` (default), `lined`, `kraft` or `plain`. |
| size | "sm" | "md" | "lg" | md | Spinner size for the inline loader. |
| slowAfter | number | 4000 | After this many milliseconds, say it's slow and offer Cancel. 0 never does. |
| slowMessage | ReactNode | "This is taking longer than usual." | What to say when it's slow. |
| tape | "none" | "periwinkle" | "mint" | "mustard" | "bubblegum" | mustard | Card only: the tape holding it down. |
| tipEvery | number | 2600 | Card only: milliseconds per tip. |
| tips | string[] | none | Card 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| value required | string | — | Matches the Tab that shows it. |
| children | ReactNode | none | — |
| className | string | none | Extra class on the panel. |
| loading | boolean | false | Still on its way: skeleton lines instead of the content, and `aria-busy`. The tabs keep working. |
| loadingLines | number | 3 | How 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.