Fylgja Skeleton
A skeleton is a set of gray shapes that stand in for content while it loads. It mirrors the layout of what is coming (an image, a title, a few lines of text), so the page does not jump once the real content arrives, and a soft shimmer shows that something is happening.
Preview
Loading article
<article class="card max-w-full" aria-busy="true" style="--size-x: 20rem">
<p class="sr-only">Loading article</p>
<div class="flex-col gap" aria-hidden="true">
<div class="skeleton" style="--size-y: 10rem"></div>
<div class="skeleton" style="--size-y: 1.5rem; --size-x: 70%"></div>
<div class="skeleton" style="--size-y: 1rem"></div>
<div class="skeleton" style="--size-y: 1rem; --size-x: 85%"></div>
</div>
</article>
<style>
:where(.skeleton) {
border-radius: 0.35rem;
}
.skeleton {
--skeleton-bg: color-mix(in oklab, var(--root-bg) 82%, var(--root-fg));
--skeleton-shine: color-mix(in oklab, var(--root-bg) 60%, transparent);
background-color: var(--skeleton-bg);
}
@media (prefers-reduced-motion: no-preference) {
.skeleton {
background-image: linear-gradient(90deg, transparent 40%, var(--skeleton-shine) 50%, transparent 60%);
background-size: 300% 100%;
animation: skeleton-shimmer 1.5s linear infinite;
}
}
@keyframes skeleton-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0 0;
}
}
</style>Usage
- Mark the region that is loading with
aria-busy="true", and put a shortsr-onlyline inside it that says what is loading. - Wrap the shapes in an element with
aria-hidden="true", and size each.skeletonwith the--size-yand--size-xvariables so it matches the content it replaces. - When the content is ready, replace the shapes and set
aria-busytofalse. - The
.skeletonrule fills the shape and adds the shimmer only when the reader has not asked for reduced motion. Its border radius is wrapped in:where(), so utilities likeroundstill win.
Variants
List
Combine a round shape for the avatar with lines of text, using the same
.skeleton rule as the preview above.
Loading comments
<section aria-busy="true">
<p class="sr-only">Loading comments</p>
<div class="flex-col gap" aria-hidden="true">
<div class="flex gap">
<div class="skeleton round flex-none" style="--size: 2.5rem"></div>
<div class="flex-col gap-sm flex-auto">
<div class="skeleton" style="--size-y: 1rem; --size-x: 40%"></div>
<div class="skeleton" style="--size-y: 1rem"></div>
</div>
</div>
<div class="flex gap">
<div class="skeleton round flex-none" style="--size: 2.5rem"></div>
<div class="flex-col gap-sm flex-auto">
<div class="skeleton" style="--size-y: 1rem; --size-x: 30%"></div>
<div class="skeleton" style="--size-y: 1rem; --size-x: 80%"></div>
</div>
</div>
</div>
</section>Anatomy
| Part | Responsibility |
|---|---|
[aria-busy="true"] |
The region that is loading. |
.sr-only text |
Tells screen reader users what is loading. |
[aria-hidden="true"] |
Hides the placeholder shapes from assistive tech. |
.skeleton |
One placeholder shape, sized with --size-y and --size-x. |
Accessibility
- The shapes are hidden with
aria-hidden, and thesr-onlytext is the only thing a screen reader finds while the region loads. aria-busyasks assistive tech to hold off on the region until it is filled.- The shimmer respects
prefers-reduced-motionand does not run for readers who turn motion off. - Never put focusable elements inside the skeleton. There is nothing to act on until the content arrives.
FAQ
Why are the shapes hidden from screen readers?
They are empty boxes with nothing to read. aria-hidden keeps them out of the accessibility tree, and a visually hidden line of text says what is loading instead.
What does aria-busy do?
It tells assistive tech the region is still being filled, so a screen reader can wait for the final content instead of announcing every change along the way. Set it to false, or remove it, once the content is in.
What happens with reduced motion?
The shimmer is only added inside a prefers-reduced-motion no-preference query, so readers who ask for less motion get the plain gray shapes.
Should every load show a skeleton?
No. For a load that is over in a moment, a skeleton only adds a flash. Use it where content takes a noticeable time and its layout is known up front, so the page does not jump when the real content arrives.