Components / feedback
Skeleton
It belongs to the loading pattern more than to itself, which is why the guidance that matters is on the Components page under Loading rather than here.
Summary
The shape of content that has not arrived.
| APG pattern | none |
| Built on | native elements |
| Status | stable |
| Since | 0.2.0 |
<Skeleton style={{ inlineSize: '60%' }} />
<Skeleton style={{ blockSize: '8rem' }} />
Anatomy
A <span data-se="skeleton" aria-hidden="true">. It has no parts — it is one block that takes its size from where you put it.
_No named parts. The frame is the whole anatomy._
Properties
_None. Everything it renders comes from its children._
States
_None. Rest is the only condition it has._
Behavior
It pulses until it is replaced.
Accessibility
Always aria-hidden, and it is not optional. A skeleton is a picture of a paragraph. Announcing it would read a layout to someone who asked for content. The announcement belongs to whatever owns the load: a Spinner beside it, or aria-busy on the region being filled.
Keyboard
None of its own. It is not focusable and takes no keys.
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-hidden | always true |
Focus
Never takes focus, and is out of the accessibility tree entirely.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/browser/feedback.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-duration-slow | animation |
--se-ease-standard | animation |
--se-radius-sm | border-radius |
--se-surface-sunk | background |
Motion
Opacity pulses to 45% and back over four times --se-duration-slow, forever. The reduced-motion reset stops it after one pass, leaving a flat block — which is still a legible placeholder.
No shimmer sweep. A gradient travelling across a page of skeletons is a lot of movement to say "not yet".
Content
None. If a placeholder needs words, the honest words are an EmptyState.
Usage
It takes its size from where you put it. The only useful skeleton is one shaped like the thing it stands in for — a skeleton the wrong shape moves the page when the content lands, which is the reflow it existed to prevent.
Match the count, not just the shape. Three lines of skeleton for one line of text is a worse guess than no skeleton. If the number of rows is unknown, one block is honest and five are a lie.
When not to use it. For a load under a second, nothing is better than either a skeleton or a spinner. For anything the reader triggered and is waiting on, use the control's own busy state.
Notes
It belongs to the loading pattern more than to itself, which is why the guidance that matters is on the Components page under Loading rather than here.