Skeleton

The shape of the content before the content arrives, so the layout does not move when it does.

Accessibility

What Skeleton does for you, and what it leaves to the page around it.

What it is in the accessibility tree

Skeleton is decoration. It has no role, no aria-busy and no live region, and the only ARIA attribute it sets is aria-hidden="true" on Skeleton.Image's svg. A screen reader user is told nothing when the bars appear.
  • The root is a plain <div>. Skeleton.Avatar, Skeleton.Button and Skeleton.Input each render an empty <span>, which is silent.
  • title is not silent. It renders an empty <h3>, which shows up in a screen reader's headings list as a blank entry, once for every Skeleton on the page.
  • Neither is paragraph. It renders a <ul> of empty <li> elements, so it is announced as a list with that many blank items.
  • Skeleton.Image's svg has a <title>Image placeholder</title>, but the svg is aria-hidden, so that text never reaches anyone. Do not rely on it as a label.
  • Where the blank heading and the empty list are noise, put aria-hidden="true" on the element you wrap the placeholder in, and announce the loading yourself.

Announce the wait somewhere

Because the placeholder says nothing, a screen reader user can land on a page that is all skeleton and hear only a blank heading and an empty list. Say once, in text, that something is loading.
  • One message per region, not one per bar. A list of twelve placeholder rows is one wait.
  • Say what is loading. “Loading profile” is useful. “Loading” on a page with three separate regions is not.
  • Say when it finishes too, in the same place. A message that announces the start and never the end leaves the reader waiting for a page that already arrived.
  • Say when it fails. A skeleton that never resolves is the one state a placeholder cannot describe, so the error has to come from the page around it.
  • Keep the message out of the tab order. It is a status, and nothing about it is actionable.

aria-busy and the live region

Both belong on the container you own, the element the Skeleton sits inside. Skeleton sets neither on itself, and it does not pass unknown props to its element, so an aria-busy passed to it is dropped.
  • aria-busy="true" on the region while it loads, removed when the content lands. It marks the region as changing but announces nothing on its own, so pair it with a message.
  • An element with role="status" (which implies aria-live="polite") inside the same region carries the words. Use polite, not assertive: a page loading is not an interruption.
  • The live region has to be on the page before its text changes. A region mounted with its message already inside is often not announced at all, so mount the empty region with the page and write into it.
  • Do not move focus when the content arrives unless the reader asked for it. A region that finished loading on its own has not earned the reader's cursor.
  • Where the whole page is a skeleton, the same pairing applies one level up: the region is the main content area, and the message says the page is loading.

The shimmer and reduced motion

active paints a gradient sweep that repeats for as long as the placeholder is mounted, and it does not check prefers-reduced-motion.
  • A continuous sweep across a large area is one of the things a reader with a vestibular disorder has asked not to see. Read the preference and pass active={!reduced}.
  • Turning motion off in the theme does not stop it. A theme that sets motion: false leaves the sweep running.
  • Turning the shimmer off removes no information, because the shimmer never carried any. The announcement does.

Contrast of the bars

Every bar paints gradientFromColor, which is rgba(0,0,0,0.06) in the default theme, and the active sweep runs up to gradientToColor at rgba(0,0,0,0.15). The dark theme swaps both for white at low alpha. The Tokens tab lists all eight tokens.
  • Both are semi-transparent fills, so they blend with whatever is behind them. The same token that reads clearly on a white page can all but vanish on a tinted card, a colored banner or an image. Check the placeholder on the surface it will sit on, in both modes.
  • No text contrast ratio applies to the bars, since they carry no text. A reader who cannot make them out still needs to know the page is loading, so announce it.
  • Keep them lighter than the text that will replace them. A bar darker than body text reads as content, and a reader waits for it to say something.
  • Do not raise the contrast with a hardcoded gray. Change gradientFromColor and gradientToColor, or the global fills they come from, so both modes change together.
  • Windows High Contrast mode drops background colors, so a skeleton can disappear there. The announcement is then the only thing left.