Skeleton
Skeleton — the grey shapes that hold a piece of layout open while its content is still loading.
Examples
Loading the article…
<section class="card" style="inline-size: 22rem" aria-busy="true">
<p class="sr-only" role="status">Loading the article…</p>
<div class="card-body" aria-hidden="true">
<div class="skeleton skeleton-block"></div>
<div class="skeleton-lines">
<span class="skeleton skeleton-text"></span>
<span class="skeleton skeleton-text"></span>
<span class="skeleton skeleton-text"></span>
</div>
</div>
</section>Shown inside the card it stands in for, because that is the only way to see it: a skeleton is inline-size: 100% and reserves the space its content will take, so on its own in a centred row it has no width to fill and paints nothing.
It also reads --color-surface-sunken, which is invisible against a sunken surface — put skeletons where the content goes, not on the page background. The shapes are aria-hidden and the state lives on the region as aria-busy; a screen reader reading eight empty grey boxes is worse than silence, and aria-busy announces nothing on its own, which is why the role="status" line is there to say what is happening.
Tokens 8
Level 2, declared on .skeleton itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --skeleton-duration | 1.6s |
| --skeleton-line-height | 1.5em |
| --skeleton-bg | var(--color-surface-sunken) |
| --skeleton-radius | var(--radius-sm) |
| --skeleton-sheen | var(--color-border) |
| --skeleton-aspect-ratio | 16 / 9 |
| --skeleton-circle-size | 2.5rem |
| --skeleton-text-thickness | 0.85em |
Variants and states
Variants
.skeleton-text.skeleton-block.skeleton-circle
Inside it
.skeleton-lines
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
data-legacy
Before you ship it
What you have to do 3 requirements
- Put
aria-hidden="true"on the outermost purely decorative wrapper, not on each bar. Never on a wrapper that also holds real content or a focusable control: that is an axe failure and a keyboard trap. - Put
aria-busy="true"on the region being filled — the element that will still be there when the content arrives — and remove it in the same update that swaps the skeletons for the content. - Say what is loading. The shapes announce nothing by design, so the region needs an
.sr-onlyrole="status"that does: "Loading posts…".
src/css/components/skeleton.css · npx mostlycss add skeleton