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 with aria-busy="true" and an aria-label describing what's loading. Screen readers announce the busy state and skip the placeholders.
  • Replace the aria-busy container's content with the real content (and remove aria-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

TokenDefaultDescription
--skeleton-colorvar(--color-fill-strong)Base fill tint for the skeleton block
--skeleton-highlightvar(--color-fill)Shimmer band tint for the animated sweep
--skeleton-radiusvar(--radius-sm)Corner radius for the skeleton block

Classes

ClassDescription
.skeletoncomponent root
.is-circleCircle placeholder for avatar or icon stand-ins