Skip to content

SkeletonBlock

<caos-skeleton-block>

a shape-preserving placeholder while data loads

May be placed on a record page, an app’s home page, a page inside an app and a step of a guided flow.

Name Attribute Description Type Default Required
variant variant, markup only Which preset shape to draw: line (a line of text), title (a taller, shorter line), block (a filled region) or circle (an avatar). Read by the stylesheet rather than by script. text — one of line, title, block, circle line Optional
w w, markup only Override the width with any CSS length or percentage. Varying it down a stack is what makes a run of lines read as text rather than as bars. text none declared Optional
h h, markup only Override the height with any CSS length. Use it to match the real content’s height exactly. text none declared Optional
radius radius, markup only Override the corner radius with any CSS length. Set it when the thing being stood in for has corners the variant presets do not match. text none declared Optional

This piece raises no events.

Nothing goes inside this piece.

Design token What it controls
--caos-color-surface-2 The base tint of the block — the colour it is when it is not shimmering.
--caos-color-surface The colour of the highlight that sweeps across it.
--caos-radius-sm The corners of the line and title variants.
--caos-radius The corners of the block variant.
--caos-radius-pill The corners of the circle variant — what makes it round.

Nothing else drives this piece by calling it.

Nothing on this piece can be read back.

Part Which piece of it When it is there
::part(block) the shimmering block Always

A REGION is waiting for data and its shape is already known. Reach for it instead of a spinner or an empty box: the placeholder holds the layout still, so nothing moves when the content lands. If what is busy is a control rather than a region, the busy state belongs on the control.

  • Button — The busy thing is a CONTROL — a save in flight. Its loading state draws the spinner, marks the control busy and stops it being pressed twice, which a placeholder beside it cannot do.
  • ProgressBar — How far along the work is can actually be measured, or it runs in named stages.
  • EmptyState — The wait is over and the answer was nothing. A placeholder that never resolves reads as a hang.
  • PagePending — The PAGE is what is waiting — a route landed on from a link, before it knows what it is going to be. There is no shape to preserve yet, so there is nothing for a placeholder to stand in for.

The block is marked aria-hidden and the host element is marked aria-busy when it connects, so a screen reader is not read a wall of meaningless boxes. The shimmer honours prefers-reduced-motion: for a reader who has opted out it is a flat tint with no sweep at all. Two things are left to the author. The aria-busy is only applied when the element carries no role attribute of its own, so setting a role suppresses it. And busy is announced per placeholder, not per region — a region that needs to announce that it is loading, and again that it has loaded, needs a live region around it.

A record section mid-load: the heading and the field labels are already known, so only the values stand in — at the widths the real values will occupy, in the rows they will occupy, so nothing moves when they land.

{
"id": "example",
"section": "A section keeping its shape",
"columns": 1,
"items": [
{
"id": "detail_section_1",
"type": "component",
"key": "detail_section",
"inputs": {
"heading": "Account information"
},
"children": [
{
"id": "key_value_row_2",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Region"
},
"children": [
{
"id": "skeleton_block_3",
"type": "component",
"key": "skeleton_block",
"inputs": {
"w": "68px",
"h": "13px"
}
}
]
},
{
"id": "key_value_row_4",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Status"
},
"children": [
{
"id": "skeleton_block_5",
"type": "component",
"key": "skeleton_block",
"inputs": {
"w": "52px",
"h": "13px"
}
}
]
},
{
"id": "key_value_row_6",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Owner"
},
"children": [
{
"id": "skeleton_block_7",
"type": "component",
"key": "skeleton_block",
"inputs": {
"w": "96px",
"h": "13px"
}
}
]
},
{
"id": "key_value_row_8",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Name"
},
"children": [
{
"id": "skeleton_block_9",
"type": "component",
"key": "skeleton_block",
"inputs": {
"w": "124px",
"h": "13px"
}
}
]
}
]
}
]
}
<caos-detail-section heading="Account information">
<caos-key-value-row label="Region">
<caos-skeleton-block w="68px" h="13px"></caos-skeleton-block>
</caos-key-value-row>
<caos-key-value-row label="Status">
<caos-skeleton-block w="52px" h="13px"></caos-skeleton-block>
</caos-key-value-row>
<caos-key-value-row label="Owner">
<caos-skeleton-block w="96px" h="13px"></caos-skeleton-block>
</caos-key-value-row>
<caos-key-value-row label="Name">
<caos-skeleton-block w="124px" h="13px"></caos-skeleton-block>
</caos-key-value-row>
</caos-detail-section>

Every preset the block has: a heading line, a text line, a filled region and an avatar circle — the four to pick between when matching the content that is coming.

{
"id": "example",
"section": "The four shapes",
"columns": 1,
"items": [
{
"id": "skeleton_block_1",
"type": "component",
"key": "skeleton_block",
"inputs": {
"variant": "title"
}
},
{
"id": "skeleton_block_2",
"type": "component",
"key": "skeleton_block",
"inputs": {
"variant": "line"
}
},
{
"id": "skeleton_block_3",
"type": "component",
"key": "skeleton_block",
"inputs": {
"variant": "block",
"w": "160px"
}
},
{
"id": "skeleton_block_4",
"type": "component",
"key": "skeleton_block",
"inputs": {
"variant": "circle"
}
}
]
}
<caos-skeleton-block variant="title"></caos-skeleton-block>
<caos-skeleton-block variant="line"></caos-skeleton-block>
<caos-skeleton-block variant="block" w="160px"></caos-skeleton-block>
<caos-skeleton-block variant="circle"></caos-skeleton-block>