Skeleton
A decorative placeholder for content that has not loaded yet. Its shape variants (text lines, circle, rectangle, and fill-the-box) combine into any card-like placeholder, with free width, height, and radius overrides and a shimmer animation that goes still under reduced motion.
Import
import { Skeleton } from "@hyzer-labs/ui"Demo
<Skeleton variant="text" />
<Skeleton variant="circle" />
<Skeleton variant="rect" />
<div style="width: 8rem; height: 4rem;">
<Skeleton variant="block" />
</div>The last line uses lastLineWidth (default 60%) so the block reads like a real
paragraph.
<Skeleton variant="text" lines={3} /><Skeleton animation="shimmer" />
<Skeleton animation="pulse" />
<Skeleton animation="none" />Skeleton is decorative (aria-hidden) and cannot announce loading on its
own. Pair it with a labeled indeterminate Loading (or a polite aria-live message) inside an aria-busy region. Screen-reader users
then hear that content is coming, and hear when it arrives.
<div aria-busy="true">
<Loading label="Loading comments" />
<Skeleton variant="text" lines={3} />
</div>There is no card-skeleton prop. Compose several Skeletons in your own Card/Stack/Cluster layout instead.
<Card>
{#snippet media()}
<div style="aspect-ratio: 16/9">
<Skeleton variant="block" />
</div>
{/snippet}
<Cluster gap="sm">
<Skeleton variant="circle" width="2.5rem" />
<Stack gap="xs">
<Skeleton variant="text" width="8rem" />
<Skeleton variant="text" lines={2} />
</Stack>
</Cluster>
</Card><div class="skeleton-table">
<div class="skeleton-table-row">
<Skeleton variant="text" width="30%" height="0.75rem" />
<Skeleton variant="text" width="20%" height="0.75rem" />
<Skeleton variant="text" width="20%" height="0.75rem" />
<Skeleton variant="text" width="15%" height="0.75rem" />
</div>
{#each { length: 4 }, i (i)}
<div class="skeleton-table-row">
<Skeleton variant="text" width="30%" />
<Skeleton variant="text" width="20%" />
<Skeleton variant="text" width="20%" />
<Skeleton variant="text" width="15%" />
</div>
{/each}
</div><Stack gap="lg">
<Stack gap="sm">
<Skeleton variant="text" width="40%" height="1.5rem" />
<Skeleton variant="text" lines={3} />
</Stack>
<Stack gap="sm">
<Skeleton variant="text" width="55%" height="1.5rem" />
<Skeleton variant="text" lines={4} />
</Stack>
</Stack>Props
| Name | Type | Default | Note |
|---|---|---|---|
variant | 'text' | 'circle' | 'rect' | 'block' | 'text' | Picks a shape and its default dimensions. For a card-like placeholder, compose several Skeletons in your own Stack/Cluster/Card layout; there is no preset prop for it. |
width | string | number | variant default | A number is emitted as px; a string is used as written (any CSS length or percentage). This is a per-instance style rather than a theme hook, like Grid's --hz-grid-cols*. |
height | string | number | variant default | See width. |
rounded | 'none' | 'sm' | 'md' | 'lg' | 'full' | variant default | The shared Rounded scale. circle always forces full, whatever you pass here. |
lines | number | 1 | text only. > 1 renders that many stacked bars; a value ≤ 0 clamps to 1 (with a warning in development). Ignored on other variants. |
lastLineWidth | string | number | '60%' | text only, lines > 1. The final line is shortened so the block reads like a real paragraph. |
animation | 'shimmer' | 'pulse' | 'none' | 'shimmer' | Every mode collapses to a static muted block under prefers-reduced-motion: reduce. |
class | string | — | Merged after the hz-skeleton class. |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-skeleton
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-variant | 'text' | 'circle' | 'rect' | 'block' | Default text. |
data-animation | 'shimmer' | 'pulse' | 'none' | Default shimmer. Collapses to a static block (same as none) under prefers-reduced-motion: reduce. |
data-rounded | 'none' | 'sm' | 'md' | 'lg' | 'full' | 1:1 with the --hz-radius-* scale. circle forces full regardless of the rounded prop. |
Custom properties
| Hook | Values | Styles |
|---|---|---|
--hz-skeleton-color | <color> | The base block color. Strengthened in dark (the Badge --hz-badge-tint precedent). |
--hz-skeleton-highlight | <color> | The shimmer sweep color. |
--hz-skeleton-speed | <time> — default 1.4s | The shimmer/pulse animation cycle. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-skeleton-line | child element | One bar in a multi-line text skeleton (variant="text", lines > 1). |
Accessibility
Skeleton stamps aria-hidden="true" by default. It is decorative scaffolding with no role, no live region, and no label, so it cannot announce loading on its own. In the rare case you want it exposed, you can override aria-hidden through the rest props.
That is the deliberate split: Loading announces, Skeleton decorates. Wrap the loading region in aria-busy="true" and pair it with either a labeled indeterminate Loading or a polite aria-live "Loading…" message, so screen-reader users are told content is coming, then told when it arrives. See the paired loading pattern demo below.
Reduced motion works the other way round here from Loading, on purpose. Skeleton's shimmer and pulse go fully still under prefers-reduced-motion: reduce, because the placeholder shape already carries the "content loading here" cue, so no motion is needed. Loading (and Button's loading spinner) keep animating instead, much more slowly, because for them the motion is the cue.
References: MDN: aria-busy · MDN: ARIA live regions