Skeleton
A loading placeholder with an opacity-only pulse — text bars, rectangles, or circles — for surfaces still waiting on data.
Import
import { Skeleton } from "@roger-emerson/ploxum-primitives";Usage
{/* KPI tile loading state */}
<div className="flex w-56 flex-col gap-3 rounded border border-[var(--ploxum-color-border-subtle)] p-4">
<Skeleton height={12} label="Loading attendance metrics" width={96} />
<Skeleton height={28} width={120} />
<Skeleton lines={2} variant="text" />
</div>
{/* Operator row loading state */}
<div className="flex items-center gap-3">
<Skeleton variant="circle" width={32} />
<Skeleton height={14} width={160} />
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "text" | "rect" | "circle" | "rect" | Shape. text renders stacked bars; circle renders square aspect. |
width | number | string | "100%" (40 for circle) | Width in px or any CSS length. |
height | number | string | 16 (40 for circle) | Height in px or any CSS length. |
lines | number | 3 | Bar count for the text variant; the last bar renders at ~60% width. |
label | string | — | Accessible loading text (sr-only, sets role="status"). When omitted the skeleton is aria-hidden. |
className | string | — | Merged onto the root. |
ref | Ref<HTMLDivElement> | — | Ref to the root. |
Design rules
- Opacity-only pulse (1600ms) — gradient shimmer sweeps are banned by the motion spec. Don't add one with
className. - Never place Skeleton inside Banner or AlertCard critical contexts. Critical surfaces render their real content or nothing — a pulsing placeholder next to a P1 reads as motion on critical state.
- The pulse is gated in CSS via
motion-safe, so it renders static underprefers-reduced-motion: reducewith no JS. - Set
labelon one skeleton per loading region and leave the rest unlabeled (aria-hidden) — one status announcement per region, not one per bar.
