Skeleton
A placeholder for a region whose content is on its way. The region is aria-busy and says what is loading, then announces the arrival once. Quiet is flat sunk blocks; warm is a pencil sketch whose outlines ink in when the content arrives; playful adds colour washes and a wobble on twos while waiting.
quiet warm playful
Open the live demo
npx susegad add skeleton
Stands on: Core, Core: components, Engine, Tokens
The prompt
the prompt
A pencil sketch of the page, inked when the real thing arrives.
A placeholder for a region whose content is on its way. While it waits, the region is aria-busy and a hidden line says what is loading. When the content arrives it shows at once, the arrival is announced, and in warm and playful the pencil outlines ink in before giving way.
<link rel="stylesheet" href="susegad/components/skeleton/skeleton.css">
<script type="module" src="susegad/components/skeleton/skeleton.js"></script>
<sg-skeleton busy shape="card" lines="2" label="the room details"></sg-skeleton>
<script>
const room = await fetch('/api/room').then(r => r.json());
skeleton.innerHTML = renderRoom(room); // the content goes inside
skeleton.busy = false; // and only then does the skeleton step aside
</script>
The prompt
Build a loading placeholder as a light-DOM custom element, <sg-skeleton busy shape="text|card|list|media" lines="3" label="…">, that wraps the region whose content is coming. While busy is set, hide the content, set aria-busy="true" on the element, and keep a visually hidden role="status" line that says "Loading" and the label; when busy is removed, show the content at once and change that line to "The room details loaded". Lay the placeholder out from the shape and a seed, in pixels for the element's width: bars for lines of text with the last one shorter, a picture block for media, a circle and two bars for each row of a list. Give it three registers. Quiet: flat blocks a shade deeper than the surface, with no shimmer and nothing moving; the content replaces them with a fade under 200 milliseconds. Warm: the same blocks drawn in SVG as two wandering pencil passes, the shape resampled and nudged by smooth noise, with light diagonal shading where a picture goes; nothing moves while waiting, and when the content arrives each outline is traced in ink with a dash offset, then the drawing crossfades to the content. Playful: soft colour washes in mango, sea, paddy and kokum, a doodled sun and two hills in the picture, and a wobble on twos while waiting: three drawings of the outlines swapped twelve times a second with step easing, run only while the region is busy and on screen; the ink is the accent colour. Show only what is true: nothing suggests an amount, the ink-in plays only on a real arrival, and with reduced motion the content simply appears. Without JavaScript, show one flat sunk block.
Words to code
| When you say | Technique | What happens |
|---|---|---|
aria-busy="true" and a visually hidden role="status" line | accessibility | Anyone reading the region hears "Loading the room details" instead of stale content. When busy goes, the same line says "The room details loaded" once, and the element fires sg-loaded. |
| lay the placeholder out from the shape and a seed | pure layout | layout(shape, { lines, width, seed }) returns plain blocks and runs in Node. The same seed gives the same widths every time, so a list does not shuffle on each render. |
| two wandering pencil passes, nudged by smooth noise | wobble | Each block's outline is resampled every 4 pixels and pushed along its normal by Perlin noise. A second pass with a different seed sits slightly off the first, as pencil lines do. |
| traced in ink with a dash offset | stroke dashing | Each ink path's dash array is its own measured length, and the Web Animations API moves the dash offset from that length to zero, one outline after another. |
| a wobble on twos | hand-drawn animation | Three versions of the outlines are drawn once. Each is shown for one twelfth of a second in turn with step-end easing, so the lines boil the way animation drawn on twos does. |
| run only while the region is busy and on screen | budget | A shared IntersectionObserver pauses the wobble off screen, and it is cancelled the moment the content arrives. |
| the ink-in plays only on a real arrival | real arrival | The arrival is driven by busy going away, never by a timer. Under reduced motion the phase goes straight to done. |
<sg-skeleton> stands in for a region while its content is loading, and steps aside the moment the content is there.
Usage
<link rel="stylesheet" href="susegad/components/skeleton/skeleton.css">
<script type="module" src="susegad/components/skeleton/skeleton.js"></script>
<sg-skeleton busy shape="list" lines="3" label="your guests">
<!-- the real list goes here when it arrives -->
</sg-skeleton>
Put the content inside the element, then remove busy (or set el.busy = false). If the content is already there when the page renders, leave busy off and nothing is drawn.
Pick the shape that looks most like what is coming, so the page does not jump when it arrives:
| shape | draws |
|---|---|
text (default) | lines bars of text, the last one shorter |
card | a picture, a title and lines bars |
list | lines rows, each a circle and two bars |
media | one 16:9 picture |
Attributes, properties and events
| Name | Type | What it does |
|---|---|---|
busy | boolean attribute, and the busy property | While set, the content is hidden, the element is aria-busy, and the placeholder shows. |
shape | text, card, list or media | The placeholder layout. Unknown values fall back to text. |
lines | number, 1 to 12 (default 3) | Lines of text, or rows of a list. |
label | text | What is loading, as a noun phrase in lower case: label="your guests" gives "Loading your guests", then "Your guests loaded". |
seed | number or text | Changes the bar widths. The same seed always gives the same placeholder. |
register | quiet, warm or playful | Overrides the page's register for this element. |
sg-loaded | event | Fires once when busy goes away, with detail: { label }. |
Registers
- Quiet: flat blocks a shade deeper than the surface. Nothing moves while waiting; the content fades in within 150 ms.
- Warm: the placeholder drawn in pencil, with light shading where a picture goes. Nothing moves while waiting. When the content arrives, each outline is traced in ink (about a third of a second), then the drawing gives way to the content.
- Playful: soft colour washes, a doodled picture, and a wobble on twos while waiting. The ink is the accent colour.
- Reduced motion: the placeholder is still, and the content appears at once when it arrives.
Accessibility
- While busy, the element has
aria-busy="true"and holds a visually hiddenrole="status"line saying what is loading. The drawing isaria-hidden. - The content is hidden with
display: nonewhile busy, so nobody reads stale or half-rendered content. - When the content arrives it joins the accessibility tree at once, even while the warm ink-in is still playing, and "… loaded" is announced once.
- Nothing in the skeleton takes focus.
What moves, and why
A skeleton does not know how far along the work is, so nothing in it suggests an amount. Playful's wobble says only that something is still coming, and it stops when the region is off screen. The ink-in is triggered by the content arriving, never by a timer.