Skip to main content

Skeleton

betav0.1.0

Decorative placeholder for content that is loading: a block or circle in the elevation highlight colour, crossed by a wave once per second. Mirror the loaded layout — same size and position as what it replaces, so nothing shifts when data arrives. Every Skeleton sits in a loading region: `SkeletonGroup`, exported alongside, keeps one `aria-busy` container and one `role="status"` announcement per region, around a whole list or grid, never one per item. Size with `h-*`/`w-*` classes only. Replaces MUI `Skeleton` and the `@filigran/ui` one. An action in progress is a `Spinner`; a known percentage is a `ProgressBar`.

Variants

shape="rectangle"
shape="circle"
SkeletonGroup — one region for the whole list

Examples

<SkeletonGroup loading={isLoading}>
  {isLoading ? (
    <div className="flex flex-col gap-2">
      <Skeleton className="h-4 w-9/10" />
      <Skeleton className="h-4 w-9/10" />
      <Skeleton className="h-4 w-3/5" />
    </div>
  ) : (
    <p className="content-base text-default-primary">Lorem ipsum dolor sit amet.</p>
  )}
</SkeletonGroup>
{/* List row with an avatar cell: MUI variant="circular" 30x30 */}
<div className="flex items-center gap-3">
  <Skeleton shape="circle" className="h-7.5 w-7.5" />
  <Skeleton className="h-4 w-9/10" />
</div>
{/* A translated, specific announcement when several regions load at once */}
<SkeletonGroup loading={isLoading} label="Loading threat actors">
  {isLoading ? <Skeleton className="h-4 w-9/10" /> : <p>Lorem ipsum dolor sit amet.</p>}
</SkeletonGroup>
{/* MUI <Skeleton variant="text" width="55%" sx={{ fontSize: 13 }} /> */}
<Skeleton className="h-3 w-11/20" />

Props

PropType
shape"rectangle" | "circle" — the Figma `shape` property. "rectangle" (default) is radius-sm; "circle" is radius-full, for avatar and round icon cells only (RULE-05).
classNamestring — carries the size: h-* and w-* classes (`h-4 w-9/10`, `h-12 w-12`, `h-full w-9/10`). No width/height props (Skeleton.rfc.md §9 Q4). Spacing between bars is gap-* on the parent, never a margin here.
refReact.Ref<HTMLSpanElement> — forwarded to the block span.
SkeletonGroup.loadingboolean, required — true while the region loads: sets aria-busy and fills the status text. Keep the group mounted and toggle this.
SkeletonGroup.labelstring — announced while loading. Default "Loading…". Pass a translated, specific label when several regions load at once (RULE-08).
SkeletonGroup.childrenReactNode — the skeletons while loading, the content after. The group does not choose; the caller does.
SkeletonGroup.refReact.Ref<HTMLDivElement> — forwarded to the region div.

Accessibility

2.1 AAWCAG pass

Skeleton is decorative: aria-hidden is enforced after the props spread, no role, never focusable. Its fill --bg-elevation-highlight is declared in decorativeFills rather than contrastPairs (AGENTS.md "Contrast — decorative fill exemption", Skeleton.rfc.md §9 Q1): WCAG 1.4.11 does not apply to a graphic that carries no information. Measured for the record against --bg-elevation-default: 1.22:1 dark layer-0, 1.26:1 dark layer-1, 1.13:1 light layer-0, 1.12:1 light layer-1. SkeletonGroup is the loading region: aria-busy on the element holding the children, and one role="status" sr-only span rendered just BEFORE it and outside it — a live region inside an aria-busy subtree may be held back until busy clears, which is when the label empties — mounted empty and filled in an effect (so a group loading from its first render is heard too), kept across loading, whose text changes from empty to `label` (default "Loading…") — a live region announces a change of its content, not its own insertion. One region per piece of content that loads together; never nested. Motion: the wave band (50 % opacity, 80/174 of a rectangle, twice a circle) lives in src/styles/motion.css and runs only under prefers-reduced-motion: no-preference; otherwise the block is a plain fill. WCAG 2.2.2: an animation shown during loading, when interaction cannot happen, is essential, and it stops when the data arrives. WCAG 2.3.1: one sweep per second at a 1.12–1.26:1 luminance step at most (the band is painted at 50 % opacity), far below both thresholds. The band's --bg-elevation-default stop is in no class, so Skeleton.test.tsx reads the motion.css rule back.