registry:ui · NativeHTML
skeleton — the placeholder, nothing more
A muted block with a terminal brightness pulse. Pure CSS, zero JS, aria-hidden by design: each placeholder is scenery. The loading contract — aria-busy on the container, or a visually-hidden live region — belongs to the region being loaded, never to each block.
$npx jixoai-ui add skeleton
https://ui.jixoai.com/r/skeleton.json
overview
Overview
A muted block with a terminal brightness pulse — the loading mirror of your real layout. The component is deliberately nothing: no lengths, no choreography API, no announcements. Geometry is your classes, the loading state is your container's semantics, and the surface is two atoms deep.
skeleton is the async-feedback pair's quiet half: while a press-button holds its loading pose or a promise toast says pending, the region
under load shows a muted, pulsing mirror of the layout that is
coming. The block itself is a bare div — pure CSS, zero JS — with
an inset hairline ring over a muted ground and a brightness
oscillation (opacity 1 → 0.45, 1.4s, ease-in-out). It is aria-hidden by contract: a placeholder is scenery,
and scenery never announces.
The eight-axis surface is wired (W3-D5, first-time on this
family) but the family owns nothing: every slot is no-own, an
explicit lane stamps the §10 carriers onto the block's style
attribute — JOINED with your own style declarations, the merge
law — and the scope flows to what you nest. Two laws hold the
line: the axis surface never manufactures geometry (width,
height and aspect stay your classes'), and the pulse answers to
the user's reduced-motion preference, not to a prop. The loading
STATE lives on your container: aria-busy="true",
or one visually-hidden live region announcing politely once.
skeleton
A loading card in the common shape — avatar block, two text lanes. The pulse is a brightness oscillation, not a shimmer sweep.
<!-- geometry is the consumer's; the block is bare -->
<Skeleton class="h-4 w-2/3" />
<!-- a loading card, the common shape -->
<div class="flex items-center gap-3" aria-busy="true">
<Skeleton class="size-10" />
<div class="flex flex-col gap-2">
<Skeleton class="h-3 w-32" />
<Skeleton class="h-3 w-20" />
</div>
</div>demo
Composition shells
Card, list and table loading mirrors — each shell is your real layout composed from bare blocks, with aria-busy on the shell container. The async-feedback pair: these shells are what shows while a PressButton is in its loading pose or a promise toast says pending.
skeleton · shells
<script lang="ts">
import Skeleton from '@ui/skeleton.svelte';
</script>
<div class="grid gap-4 sm:grid-cols-3">
<div class="border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">card shell</span>
<div class="mt-3 flex flex-col gap-3" aria-busy="true">
<Skeleton class="h-28 w-full" />
<div class="flex items-center gap-3">
<Skeleton class="size-8" />
<div class="flex flex-1 flex-col gap-2">
<Skeleton class="h-3 w-2/3" />
<Skeleton class="h-3 w-1/3" />
</div>
</div>
</div>
</div>
<div class="border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">list shell</span>
<div class="mt-3 flex flex-col gap-3" aria-busy="true">
{#each [64, 92, 78, 85] as w (w)}
<div class="flex items-center gap-3">
<Skeleton class="size-6" />
<Skeleton class="h-3" style="width: {w}%" />
</div>
{/each}
</div>
</div>
<div class="border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">table shell</span>
<div class="mt-3 flex flex-col gap-2" aria-busy="true">
<div class="grid grid-cols-3 gap-2">
<Skeleton class="h-3" />
<Skeleton class="h-3" />
<Skeleton class="h-3" />
</div>
{#each [0, 1, 2] as row (row)}
<div class="grid grid-cols-3 gap-2 border-t border-border/60 pt-2">
<Skeleton class="h-3 w-4/5" />
<Skeleton class="h-3 w-full" />
<Skeleton class="h-3 w-2/3" />
</div>
{/each}
</div>
</div>
</div>Geometry is always the consumer's — the blocks are bare, the shells are your layout, and the
loading STATE lives on the shell (aria-busy="true"), never
per block.
law
The scenery contract
The block is decoration; the loading state is semantics. Everything the component does fits in two atoms, one keyframe, and a merge law — measured, not asserted.
| Property | Type | Default | Description |
|---|---|---|---|
| the scenery split | aria-hidden | every block is decoration — aria-hidden lands AFTER the rest spread so a consumer's id/data-* pass through untouched while the block itself never announces. The loading STATE is semantics: aria-busy on your container, or one visually-hidden live region | announces: the block is never announced; the container carries the contract |
| the geometry | class · parent layout | the element is a bare block — no length API exists. Width, height and aspect come from your classes or the parent grid; the axis surface never manufactures a box | announces: geometry is yours, full stop |
| the pulse | jx-skeleton-pulse | a brightness oscillation (opacity 1 → 0.45 → 1, 1.4s ease-in-out, infinite) — not a shimmer sweep. Keyframes + the animation live in skeleton.css (D1-exempt: media-touched property), whose literals have no ladder step (reported MISSING --motion-1400/--motion-ease-in-out for serial promotion) | announces: measured in pixels below |
| the freeze | prefers-reduced-motion: reduce | the unlayered kill sets animation: none — the block becomes a static muted surface. The kill rides the media query, untouched by the motion lane: scenery has no choreography to dial | announces: reduced motion is a still frame |
| the merge law | carriers · your style | the W3-D5 carriers JOIN the consumer style attribute (one style channel, no fight): an explicit lane stamps --jx-*-effective alongside your own declarations — measured below | announces: the block stamps and supplies; it never repaints you |
| the paint | two atoms | a muted ground (--jx-muted) and an inset hairline ring (box-shadow inset var(--hairline) var(--border)) — the entire visual vocabulary. Mirror law: the atom table is byte-identical in registry and app trees | announces: the placeholder, nothing more |
types
Types
One bare block — every shape (avatar, lane, card) is your geometry via class or parent layout.
skeleton · types
<script lang="ts">
import Skeleton from '@ui/skeleton.svelte';
</script>
<div class="flex flex-wrap items-start gap-6">
<div class="flex min-w-52 flex-col gap-3 border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">avatar block</span>
<Skeleton class="size-10" />
<span class="text-muted-foreground text-[12.5px]">class="size-10"</span>
</div>
<div class="flex min-w-52 flex-col gap-3 border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">text lanes</span>
<div class="flex flex-col gap-2">
<Skeleton class="h-3 w-32" />
<Skeleton class="h-3 w-20" />
</div>
<span class="text-muted-foreground text-[12.5px]">class="h-3 w-32" and friends</span>
</div>
<div class="flex min-w-52 flex-col gap-3 border border-border p-4">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">full-bleed card</span>
<div class="flex flex-col gap-2">
<Skeleton class="h-3 w-full" />
<Skeleton class="h-3 w-3/4" />
</div>
<span class="text-muted-foreground text-[12.5px]">width/aspect from the parent layout</span>
</div>
</div>usage
Usage
Compose blocks into the loading mirror of your real layout; put aria-busy on the container.
<!-- geometry is the consumer's; the block is bare -->
<Skeleton class="h-4 w-2/3" />
<!-- a loading card, the common shape -->
<div class="flex items-center gap-3" aria-busy="true">
<Skeleton class="size-10" />
<div class="flex flex-col gap-2">
<Skeleton class="h-3 w-32" />
<Skeleton class="h-3 w-20" />
</div>
</div>a11y
Accessibility
Placeholder blocks are decoration; the loading STATE is semantics that belongs to the container.
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-hidden | true | Set on every block — placeholder scenery is never announced |
| aria-busy | "true" | The consumer puts it on the loading container — the contract home |
| live region | optional | A visually-hidden "loading…" region announces politely once, if aria-busy is not enough |
api
API
The Props interface adds nothing — a bare block with class passthrough and full attribute spread.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| class | string | '' | Class passthrough — width/height/aspect live here (no length API). |
| ...rest | HTMLAttributes<HTMLDivElement> | spread | data-*, id, and the rest pass through untouched; aria-hidden lands after the spread. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on skeleton
The zero-vocabulary family, W3-D5 first-time wiring: all eight slots no-own, an explicit lane stamps the §10 carriers onto the style attribute — JOINED with your own declarations — and the scope flows to what you nest. The surface never manufactures geometry; the pulse answers to the user, not the prop.
| Property | Type | Default | Description |
|---|---|---|---|
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'auto' | STAMP-ONLY, RUNG — the resolved lane stamps the §4 legacy rung on the block (measured: density="small" → data-density="sm"); the block itself reads no channel — the stamp is the SCOPE for what you nest inside. The declared-open channel with no family opinion. Number unit: coefficient. |
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | STAMP-ONLY, VOICE — the §11 pair (--jx-size-effective + font-size) lands on the root (measured 18px computed on the size demo) but NEVER manufactures width/height: the box stays exactly your classes' geometry (measured unchanged). Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY — the alias + §14 radius factor land on the root for what you nest; the bare block paints no corner of its own (the hairline ring follows your radius lane if you supply one). Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY — an explicit lane stamps --jx-radius-effective on the root (measured 12px on the stamps demo); the block has no border-radius declaration of its own, so the lane's work is the scope it supplies. Number unit: px. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY — --jx-color-effective lands on the root; the placeholder's ground is --jx-muted (the typed token), hue-neutral by contract: a loading mirror takes the context's paint, not a brand voice. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | BRIDGE — class:dark lands on the root (measured) and the muted ground re-voices through the token; scenery follows the region it loads for. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY — the §7 consumption pair stamps on the root; the block owns no shadow (the hairline ring is inset, not lifted). A loading mirror sits IN the plane it stands in for. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLIED, NOT READ BY THE PULSE — the carrier lands on the root, but the pulse is a fixed 1.4s keyframe in the family css and the reduced-motion kill rides the media query untouched by the lane (both measured below). The one motion that matters is the USER's, not the prop's. Number unit: coefficient. |
| Token | Default | Source |
|---|---|---|
| --jx-muted | the ground | color |
| the hairline ring | inset box-shadow | component |
| jx-skeleton-pulse | 1.4s ease-in-out infinite | component |
| the reduced-motion kill | unlayered :where | structural |
| data-jx hooks | NONE | component |
skeleton · universal props
<Skeleton class="h-4 w-2/3" size={18} />The stamps panel is the merge law made visible: radius={12} stamps --jx-radius-effective: 12px while the consumer's own style="border-radius: 3px" rides the SAME attribute — carrier and consumer declarations co-exist, and the visible corner is yours.