Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
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/skeleton

Usage

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):

Skeleton — data-slot="skeleton" | "skeleton-line"
SkeletonReveal — data-slot="skeleton-reveal-content"

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

PropTypeDefaultDescription
countnumber1Render 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.
shapeSkeletonShape"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

AttributeValues
data-countmirrors a prop or state value
data-shapemirrors a prop or state value
data-slot"skeleton" | "skeleton-line"

SkeletonReveal

PropTypeDefaultDescription
loading*booleanWhether the skeleton placeholder (vs. the real content) should render. Flip this to false once the data has arrived.
skeleton*React.ReactNodeThe placeholder shown while loading is true — typically one or more <Skeleton> elements shaped like the content they stand in for.
childrenReact.ReactNodeThe real content, rendered — and gently revealed via motion-enter-up — once loading is false.

Data attributes and CSS variables on SkeletonReveal

AttributeValues
data-slot"skeleton-reveal-content"

Accessibility

  • Each skeleton is decorative — rendered with role="presentation" and aria-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 in base.css zeroes 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.
ContractStates tested
Behaviourdefault, loading
Accessibilitybusy
Visualdefault, loading

Do / Don't

Do
Mirror the real content's shape and size, mark the container aria-busy, and keep the placeholder decorative.
Don't
Animate a bare spinner-less skeleton without aria-busy, or use a different layout than the loaded content (causing a jarring shift).

On this page