Skeleton
Part of the extras bundle, not included by default.
Intent
Skeletons are flat placeholder shapes that hold layout in place while real content is fetching. They register as "something's coming" without spelling out what -- avoiding both the layout jank of empty space and the over-promise of fake content.
The primitive is small on purpose: a single
.skeleton class for rectangular bars, a circle
variant for avatar and icon stand-ins, and a wrapper class
that animates a shimmer sweep across every skeleton inside.
Multi-line text placeholders compose from stacked bars rather
than a dedicated variant -- a single tall block sized for N
lines doesn't look like multi-line text, so brio leaves the
composition to the caller.
Basic usage
Set inline-size and (optionally)
block-size on each skeleton to match the shape of
the content it stands in for. The default
min-block-size is 1rem, so a width-
only skeleton renders as a single-line text bar without
extra wiring.
<div class="flow">
<div class="skeleton" style="inline-size: 12rem"></div>
<div class="skeleton" style="inline-size: 9rem"></div>
</div>Circle
.is-circle sets the radius to
--radius-pill and sizes both axes from a single
--skeleton-size token. Override the token for
different diameters.
<div class="cluster">
<div class="skeleton is-circle"></div>
<div class="skeleton is-circle" style="--skeleton-size: 3.5rem"></div>
<div class="skeleton is-circle" style="--skeleton-size: 5rem"></div>
</div>Animated
Wrap one or more skeletons in .skeleton-shimmer to
animate a gradient sweep across every skeleton descendant. The
shimmer pauses (without disappearing) under
prefers-reduced-motion: reduce; the gradient
stripe stays visible so the loading affordance remains.
<div class="skeleton-shimmer flow">
<div class="skeleton" style="inline-size: 14rem; block-size: 1rem"></div>
<div class="skeleton" style="inline-size: 11rem; block-size: 1rem"></div>
<div class="skeleton" style="inline-size: 8rem; block-size: 1rem"></div>
</div>Multi-line text
Compose stacked bars with varying widths to suggest a paragraph. The bottom row is conventionally the shortest, mirroring how real prose ends mid-line.
<div class="skeleton-shimmer stack is-tight">
<div class="skeleton" style="inline-size: 95%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 88%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 60%; block-size: 0.875rem"></div>
</div>Composing patterns
Skeleton is a primitive; real-world placeholders compose it with brio's layout primitives. A few shapes you'll reach for repeatedly:
List item
Avatar circle plus a name line and detail line, laid out with
.media.
<div class="skeleton-shimmer stack">
<div class="media">
<div class="skeleton is-circle media-figure"></div>
<div class="media-body stack is-tight">
<div class="skeleton" style="inline-size: 40%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 70%; block-size: 0.75rem"></div>
</div>
</div>
<div class="media">
<div class="skeleton is-circle media-figure"></div>
<div class="media-body stack is-tight">
<div class="skeleton" style="inline-size: 55%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 60%; block-size: 0.75rem"></div>
</div>
</div>
<div class="media">
<div class="skeleton is-circle media-figure"></div>
<div class="media-body stack is-tight">
<div class="skeleton" style="inline-size: 35%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 80%; block-size: 0.75rem"></div>
</div>
</div>
</div>Card
Image placeholder, title bar, body lines, and a footer cluster.
<docs-resize>
<div class="card skeleton-shimmer" aria-busy="true" aria-label="Loading">
<div class="skeleton" style="block-size: 8rem; border-radius: var(--radius-md) var(--radius-md) 0 0"></div>
<div class="stack" style="padding: var(--space-md)">
<div class="skeleton" style="inline-size: 60%; block-size: 1.25rem"></div>
<div class="stack is-tight">
<div class="skeleton" style="inline-size: 90%; block-size: 0.875rem"></div>
<div class="skeleton" style="inline-size: 70%; block-size: 0.875rem"></div>
</div>
<div class="cluster">
<div class="skeleton is-circle" style="--skeleton-size: 1.75rem"></div>
<div class="skeleton" style="inline-size: 5rem; block-size: 0.75rem"></div>
</div>
</div>
</div>
</docs-resize>Table rows
Wrap <tbody> in
.skeleton-shimmer so all the row cells animate
together.
| Name | Role | Status | Joined |
|---|---|---|---|
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
<th>Joined</th>
</tr>
</thead>
<tbody class="skeleton-shimmer" aria-busy="true">
<tr>
<td><div class="skeleton" style="inline-size: 80%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 60%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 50%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 70%; block-size: 0.75rem"></div></td>
</tr>
<tr>
<td><div class="skeleton" style="inline-size: 65%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 75%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 40%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 55%; block-size: 0.75rem"></div></td>
</tr>
<tr>
<td><div class="skeleton" style="inline-size: 90%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 50%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 65%; block-size: 0.75rem"></div></td>
<td><div class="skeleton" style="inline-size: 45%; block-size: 0.75rem"></div></td>
</tr>
</tbody>
</table>
</div>Customization
The base block tint and shimmer band tint flow through
--skeleton-color and
--skeleton-highlight; both default to brio's fill
scale (--color-fill-strong and
--color-fill). Override either on a parent
scope to retone the loading affordance for a specific surface
-- e.g., on a card with a tinted background where the default
fills wouldn't read.
Corner radius (--skeleton-radius) defaults to
--radius-sm; the circle variant fixes its own
radius via --radius-pill. The shimmer animation
duration and easing are set on the wrapper rule -- override
the animation shorthand on
.skeleton-shimmer .skeleton if a slower or
snappier sweep fits the surrounding pace.
Accessibility
-
Skeletons are decorative -- they convey "loading" visually
but carry no semantic content. Either add
aria-hidden="true"on each skeleton element, or (more commonly) wrap the loading region in a container witharia-busy="true"and anaria-labeldescribing what's loading. Screen readers announce the busy state and skip the placeholders. -
Replace the
aria-busycontainer's content with the real content (and removearia-busy) when the data arrives. Don't leave skeletons mounted beneath the real content -- assistive tech may double- announce. -
The shimmer animation pauses (but doesn't disappear)
under
prefers-reduced-motion: reduce. The static gradient stripe still reads as "loading" without the motion.
Markup
<!-- Single bar -->
<div class="skeleton" style="inline-size: 12rem; block-size: 1rem"></div>
<!-- Circle -->
<div class="skeleton is-circle"></div>
<div class="skeleton is-circle" style="--skeleton-size: 3.5rem"></div>
<!-- Animated group with busy state -->
<div class="skeleton-shimmer stack" aria-busy="true" aria-label="Loading users">
<div class="skeleton" style="inline-size: 80%; block-size: 1rem"></div>
<div class="skeleton" style="inline-size: 60%; block-size: 1rem"></div>
</div>
CSS Reference: Skeleton
Placeholder block for content that is still loading
Source: src/css/extras/skeleton.css
Tokens
| Token | Default | Description |
|---|---|---|
--skeleton-color | var(--color-fill-strong) | Base fill tint for the skeleton block |
--skeleton-highlight | var(--color-fill) | Shimmer band tint for the animated sweep |
--skeleton-radius | var(--radius-sm) | Corner radius for the skeleton block |
Classes
| Class | Description |
|---|---|
.skeleton | component root |
.is-circle | Circle placeholder for avatar or icon stand-ins |