jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — skeleton

$npx jixoai-ui add skeleton

https://ui.jixoai.com/r/skeleton.json

pure CSS aria-hidden scenery reduced-motion freeze

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.


skeleton-usage.svelte svelte
<!-- 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

card shell
list shell
table shell
skeleton-shells-demo.svelte svelte
<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.

PropertyTypeDefaultDescription
the scenery split aria-hiddenevery 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 regionannounces: the block is never announced; the container carries the contract
the geometry class · parent layoutthe 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 boxannounces: geometry is yours, full stop
the pulse jx-skeleton-pulsea 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: reducethe 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 dialannounces: reduced motion is a still frame
the merge law carriers · your stylethe W3-D5 carriers JOIN the consumer style attribute (one style channel, no fight): an explicit lane stamps --jx-*-effective alongside your own declarations — measured belowannounces: the block stamps and supplies; it never repaints you
the paint two atomsa 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 treesannounces: the placeholder, nothing more

types

Types

One bare block — every shape (avatar, lane, card) is your geometry via class or parent layout.

skeleton · types

avatar blockclass=xvm51jw xwaiz5r
text lanes
class=xvle69y x1eyvef and friends
full-bleed card
width/aspect from the parent layout
skeleton-types-demo.svelte svelte
<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.

Skeleton usage
<!-- 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

AttributeValueDescription
aria-hiddentrueSet on every block — placeholder scenery is never announced
aria-busy"true"The consumer puts it on the loading container — the contract home
live regionoptionalA 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

PropertyTypeDefaultDescription
class string''Class passthrough — width/height/aspect live here (no length API).
...rest HTMLAttributes<HTMLDivElement>spreaddata-*, id, and the rest pass through untouched; aria-hidden lands after the spread.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity 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.

PropertyTypeDefaultDescription
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.
TokenDefaultSource
--jx-mutedthe groundcolor
the hairline ringinset box-shadowcomponent
jx-skeleton-pulse1.4s ease-in-out infinitecomponent
the reduced-motion killunlayered :wherestructural
data-jx hooksNONEcomponent

skeleton · universal props

universal-props-demo.svelte svelte
<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.

jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h