Skeleton
A token-driven loading placeholder — line, circle, rect, and card shapes, a configurable line count, and a pulse that stops under reduced motion.
- Status
- Since
0.1.0- Accessibility pattern
- decorative placeholder (aria-hidden)
Last updated
Install
Add Skeleton from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/skeletonUsage
import { Skeleton } from '@/components/ui/skeleton';
// A single line
<Skeleton />
// A three-line paragraph
<Skeleton count={3} />;Skeleton is purely presentational and server-safe. Mark the surrounding region
aria-busy="true" while data loads so assistive tech announces the loading state —
the skeletons themselves are decorative and hidden from the accessibility tree.
Anatomy
Skeleton is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Shapes
line (a single text line, the default), circle (an avatar/icon placeholder),
rect (an image/thumbnail block), and card (a larger card surface). Every shape
is built from semantic tokens (bg-muted, rounded-*) — never a hardcoded color
or pixel value.
Composing a loading card
Combine shapes to mirror the real content's structure — an avatar, a couple of short lines, a thumbnail, and a paragraph. Matching the eventual layout prevents content from shifting when it loads in.
Line count
Pass count to render a stacked block of text lines. count is clamped to a
minimum of 1 (non-integer values are floored), and the final line is shortened
to w-4/5 to mimic the ragged end of a paragraph.
<Skeleton count={4} />Count across shapes
count stacks any shape vertically, but the ragged-end shortening only applies to
the line shape — circle, rect, and card repeat their full footprint so the
placeholder still mirrors the real content geometry.
line
circle
rect
Skeleton reveal
Skeleton itself stays a static placeholder — the reveal motion belongs to the content that
replaces it. The smallest honest way to wire this is a keyed swap:
{
loading ? (
<Skeleton count={3} />
) : (
<div className="motion-enter-up">{content}</div>
);
}SkeletonReveal is a thin convenience wrapper around exactly that pattern — it keys the
loading/content swap so it always remounts (never just patches props in place) and applies
motion-enter-up to the revealed content, so it fades + rises in instead of popping flatly into
place. Reach for the inline version instead when you don't need the shared wrapper (e.g. the
content root already needs its own distinct element/props per branch).
import { Skeleton, SkeletonReveal } from "@/components/ui/skeleton";
<SkeletonReveal loading={isLoading} skeleton={<Skeleton count={3} />}>
<Article content={data} />
</SkeletonReveal>;Playground
Switch between the four shapes and stack multiple placeholders, then copy the generated JSX.
<Skeleton />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 1 | Render this many stacked placeholders. With count > 1, a vertical stack of
count skeletons is rendered (the last line is shortened to mimic a
paragraph). Useful for multi-line text blocks. |
shape | SkeletonShape | "line" | Placeholder shape.
- line: a single text line (default).
- circle: a circular avatar/icon placeholder.
- rect: a rectangular image/thumbnail block.
- card: a larger card-surface block. |
Data attributes and CSS variables on Skeleton
| Attribute | Values |
|---|---|
data-count | mirrors a prop or state value |
data-shape | mirrors a prop or state value |
data-slot | "skeleton" | "skeleton-line" |
SkeletonReveal
| Prop | Type | Default | Description |
|---|---|---|---|
loading* | boolean | — | Whether the skeleton placeholder (vs. the real content) should render.
Flip this to false once the data has arrived. |
skeleton* | React.ReactNode | — | The placeholder shown while loading is true — typically one or more
<Skeleton> elements shaped like the content they stand in for. |
children | React.ReactNode | — | The real content, rendered — and gently revealed via motion-enter-up —
once loading is false. |
Data attributes and CSS variables on SkeletonReveal
| Attribute | Values |
|---|---|
data-slot | "skeleton-reveal-content" |
Accessibility
- Each skeleton is decorative — rendered with
role="presentation"andaria-hidden="true"so screen readers skip the placeholder geometry. - Convey the loading state on the container, not the skeleton: set
aria-busy="true"on the region being populated and provide a visually-hidden "Loading…" label or live-region announcement. - Match the skeleton's layout to the real content so focus order and scroll position stay stable once data arrives — avoid layout shift.
- The pulse respects
prefers-reduced-motion: the global reset inbase.csszeroes every animation, so the placeholder stays static. The component does not restate that rule — one reset owns it, so it can never drift per component.
| Contract | States tested |
|---|---|
| Behaviour | default, loading |
| Accessibility | busy |
| Visual | default, loading |