Skip to content

Skeleton

hc-skeleton is a placeholder you render in place of content while it loads. Apply .hc-skeleton to any element and give it a size — the component supplies the surface color, corner radius, and animation. It is pure CSS with no JavaScript: you swap the skeleton for the real markup yourself (an htmx swap, a framework re-render, etc.).

The surface uses the theme-adaptive muted background, so skeletons read correctly in both light and dark mode with no extra work.

Size the block from the consumer side — inline style, a utility class, or a wrapping layout. A skeleton has no intrinsic size.

data-shape accepts rect (default), text, and circle.

  • rect — a generic block with the medium corner radius. Use it for cards, images, and thumbnails (size it yourself).
  • text — a single text line: 1em tall with a tighter radius. Stack several with decreasing widths to mock a paragraph.
  • circle — fully rounded with aspect-ratio: 1. Set one dimension (e.g. inline-size) and it stays square — for avatar / icon slots.

data-animation accepts pulse (default), wave, and none.

  • pulse — the whole block fades in and out.
  • wave — a lighter highlight band sweeps across the block. The highlight is derived from the base color via color-mix(), so it tracks the active theme automatically.
  • none — a static block. Motion stays off regardless of the OS setting — handy when a page is dense with skeletons.

Both pulse and wave collapse to a static block under prefers-reduced-motion: reduce, so motion-sensitive users see a flat placeholder.

A skeleton is just markup. Render it in the initial response, then let htmx replace it once the real fragment arrives:

<div
data-hx-get="/dashboard/stats"
data-hx-trigger="load"
data-hx-swap="outerHTML"
role="status"
aria-busy="true"
aria-label="Loading statistics">
<div class="hc-skeleton" data-shape="text" style="inline-size:40%"></div>
<div class="hc-skeleton" style="block-size:6rem;margin-block-start:.5rem"></div>
</div>

The server returns the finished <div> (without aria-busy); the swap removes the skeletons along with the loading state.

  • Skeletons are decorative. Don’t annotate each block. Instead mark the loading region with role="status", aria-busy="true", and an accessible name (aria-label="Loading…", or visually-hidden text if you ship a utility for it). Screen readers then announce the loading state once, not once per placeholder.
  • Remove aria-busy (or swap the whole region) when the real content arrives so assistive tech knows loading finished.
  • Both animations honour prefers-reduced-motion: reduce.

Component tokens (in component.tokens.json):

Token pathPurpose
skeleton.bgBase surface — var(--hc-color-muted-bg), adapts light / dark.
skeleton.highlightWave sweep color, derived from bg via color-mix().
skeleton.radiusCorner radius for the rect shape.
skeleton.text-radiusCorner radius for the text shape.
skeleton.text-heightLine height for the text shape (1em).
skeleton.pulse-durationPulse cycle duration.
skeleton.wave-durationWave sweep duration.
Show the generated CSS variables
--hc-skeleton-bg | -highlight
--hc-skeleton-radius | -text-radius | -text-height
--hc-skeleton-pulse-duration | -wave-duration
  • Spinner — for an indeterminate inline indicator when there is no content shape to mock.
  • Progress — when you know how far along a task is.