Skeleton
A single muted block that pulses softly until real content arrives. Pick a shape (rect, circle, text), set a width and height, and stack many of them together to mock a card, an avatar row, a paragraph, or any other layout. Reach for PageSkeleton when you want a pre-built page-shaped scaffold instead — Skeleton is the primitive underneath.
Shapes
Card placeholder
Avatar row
Installation
npx shadcn@latest add https://craftbits.dev/r/skeleton.jsonUsage
import { Skeleton } from "@craft-bits/core";
<Skeleton />Compose into a card placeholder:
<div className="rounded-cb-md border border-cb-border-muted p-4">
<Skeleton shape="rect" height={140} className="mb-3" />
<Skeleton shape="text" width="60%" className="mb-2" />
<Skeleton shape="text" width="90%" />
</div>Avatar + text row:
<div className="flex items-center gap-4">
<Skeleton shape="circle" height={48} />
<div className="flex flex-1 flex-col gap-2">
<Skeleton shape="text" width="40%" />
<Skeleton shape="text" width="70%" />
</div>
</div>Understanding the component
- Three shapes.
rect(rounded-cb-md) is the default — use it for cards, images, blocks.circle(rounded-cb-full) forceswidth = heightso the result is a perfect circle, ideal for avatars and dots.text(rounded-cb-full, default height0.75rem) is a short pill sized for a single line of copy. - Sizing.
widthandheightaccept either anumber(interpreted as pixels) or a CSSstring("100%","12ch","calc(100% - 1rem)"). When omitted,widthfalls back to100%of the container (or toheightforcircle), andheightfalls back to a shape-appropriate default. - Pulse. A single global
@keyframes cb-skeleton-pulserule cycles opacity1 → 0.55 → 1over1.6s. The rule is injected into<head>once, on first client render, and is scoped under@media (prefers-reduced-motion: no-preference)— so reduced-motion users see a static tinted block with zero JS branching. - Compose, don't extend. Need a paragraph? Render three
Skeleton shape="text"s with different widths. Need a card? Wrap them in your own border + padding. The primitive deliberately stays a leaf — higher-level layouts (PageSkeleton,AppLoadingScreen) are built on top of it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
shape | 'rect' | 'circle' | 'text' | 'rect' | Visual shape of the placeholder. circle forces width = height; text defaults height to 0.75rem. |
width | number | string | '100%' | CSS width. Numbers → pixels, strings pass through. Ignored for circle (matches height). |
height | number | string | shape default | CSS height. Numbers → pixels, strings pass through. Defaults: rect → 1rem, circle → 2.5rem, text → 0.75rem. |
className | string | — | Merged onto the root <div> via cn() — overrides defaults thanks to tailwind-merge. |
Accessibility
- Renders as a
<div role="status" aria-busy="true" aria-hidden="true">. The block itself is hidden from screen readers — wrap your skeleton scaffold in a parentaria-liveregion with an accessible label ("Loading article","Loading avatars") so assistive tech announces what is loading, not the geometry of each placeholder. prefers-reduced-motion: reduceshort-circuits the pulse via a CSS-level@mediagate — no JS branching, no flash of static-then-animated.- Color contrast: placeholders use
--cb-bg-mutedagainst the default surface. The component carries no foreground text — contrast requirements apply only to your real content once it loads. - Keyboard: not focusable. The skeleton is decorative — focus should remain on the surrounding shell (page chrome, app frame) until the real content mounts and claims its own tab stops.
Credits
- Extracted from:
algoflashcards(src/platform/ui/Skeleton.tsx). The source was a 12-line<div className="animate-pulse bg-muted/40" />exported alongside two product-specific scaffolds (SkillTreeSkeleton,LessonSkeleton) that hand-rolled circles + connecting lines and reader-page card shells. The library version keeps only the primitive, generalises the API to ashapeenum + explicitwidth/height, swaps Tailwind'sanimate-pulsefor a project-token-aware@keyframes cb-skeleton-pulserule, and routes consumers toPageSkeletonfor the layout-shaped variants.