Skeleton
A skeleton is a gray shape that stands in for content while it loads. You build loading layouts from it.
<div style="display: flex; gap: var(--space-12); align-items: center"> <tk-skeleton shape="circle" style="flex: none"></tk-skeleton> <div style="flex: 1"><tk-skeleton lines="2"></tk-skeleton></div> </div>
Anatomy
| Part | What it is |
|---|---|
tk-skeleton |
one shape; hidden from screen readers, not focusable |
| bar, circle, or rectangle | the gray fill |
| lines | with lines above 1, stacked bars; the last is 60% wide |
Shapes
<tk-skeleton></tk-skeleton> <tk-skeleton lines="3"></tk-skeleton> <tk-skeleton shape="circle"></tk-skeleton> <tk-skeleton shape="rect" style="height: 140px"></tk-skeleton>
shape |
Stands in for | Size |
|---|---|---|
text (default) |
a line of text | follows the surrounding font, so it matches any text style |
circle |
an avatar or an icon | 32px, the default Avatar size; set one side to resize it |
rect |
media, a card, a field | full width; set its height with CSS |
- Size skeletons with CSS: an inline
style, a class, or the grid or flex layout around them. There are no size attributes. - Match the real content: the same grid, gaps, boxes, and line counts. Then nothing jumps when the content arrives.
linesstacks text bars at the font's own line spacing and shortens the last one, so the block reads as a paragraph. It works ontextonly. For different widths per line, use several single bars.
Corners
Text bars get radius-4, rectangles radius-8, circles radius-full. To match content
with other corners, set --tk-skeleton-radius.
<tk-skeleton shape="rect" style="height: 140px; --tk-skeleton-radius: var(--radius-12)"></tk-skeleton> <tk-skeleton shape="rect" style="height: 32px; width: 96px; --tk-skeleton-radius: var(--radius-full)"></tk-skeleton>
Layouts
A card with media and text:
<div aria-busy="true"> <tk-skeleton shape="rect" style="height: 140px"></tk-skeleton> <tk-skeleton lines="3" style="margin-block-start: var(--space-12)"></tk-skeleton> </div>
- A list: repeat the row from the top of this page. Keep row dividers and icons real.
- A table: one text bar per cell under the real headers, with a different width per column so the columns still read as columns.
States
<tk-skeleton lines="2"></tk-skeleton> <tk-skeleton lines="2" effect="none"></tk-skeleton>
pulse(default): the shape fades gently in and out. Shapes added at the same time pulse in step.effect="none": a still fill. Use it when many shapes on one screen would pulse at once.- With reduced motion, every skeleton is still.
When to use
- Use skeletons for the first load of a content region: a list, cards, a table, a block of text. Lay them out the way the real content will sit.
- Use a
Spinner or aProgress bar for an action the user started, such as saving or submitting. - Replace only the dynamic content. Navigation, page titles, and column headers render for real.
- Don't use skeletons for toasts, menus, dialogs, other loaders, or usually for buttons. Empty space works for minor items.
- Show them for a few seconds at most. For a longer wait, say what's happening in words.
Do / Don't
| ✅ Do | ❌ Don't |
|---|---|
| Build the skeleton from the same layout as the content | Invent a layout the content won't match |
| Replace only the dynamic content | Put skeletons in toasts, menus, dialogs, or static text |
Set aria-busy on the loading region |
Try to name or announce a skeleton |
| Match line counts and box sizes | Use one bar for a whole card |
Use effect="none" when many shapes are on screen |
Let dozens of shapes pulse at once |
| Use skeletons for a short first load | Leave them up for a long wait |
Related
Spinner : feedback for an action in progress.Progress bar andProgress ring : progress on a known scale.Empty state : what to show when the region loads with nothing in it.
API
Attributes
| Attribute | Type | Default | What it does |
|---|---|---|---|
shape |
text | circle | rect |
text |
the shape |
lines |
number | 1 |
text only: how many bars |
effect |
pulse | none |
pulse |
pulsing or still |
Slots
None.
Events
None.
CSS
--tk-skeleton-radius: the corner radius, for any shape.
There is no loading attribute: show skeletons while loading and remove them when the
content arrives.
Tokens
| Part | Token |
|---|---|
| fill | surface/sunken |
| corners | radius-4 (text), radius-8 (rect), radius-full (circle) |
| pulse | duration-2000, ease-in-out |
Accessibility
Built in:
- Every skeleton has
aria-hidden="true", no role, and no focus. It says nothing to screen readers.
Your part:
- Set
aria-busy="true"on the region that is loading, and remove it when the content arrives. - If the wait needs announcing, add a
role="status"message to the region, visible or visually hidden, and update it when loading finishes or fails.
Loading results
<section aria-busy="true" aria-label="Results"> <p role="status">Loading results</p> <tk-skeleton lines="3"></tk-skeleton> </section>
The gray fill is decorative, so no contrast minimum applies.