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.json

Usage

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

  1. Three shapes. rect (rounded-cb-md) is the default — use it for cards, images, blocks. circle (rounded-cb-full) forces width = height so the result is a perfect circle, ideal for avatars and dots. text (rounded-cb-full, default height 0.75rem) is a short pill sized for a single line of copy.
  2. Sizing. width and height accept either a number (interpreted as pixels) or a CSS string ("100%", "12ch", "calc(100% - 1rem)"). When omitted, width falls back to 100% of the container (or to height for circle), and height falls back to a shape-appropriate default.
  3. Pulse. A single global @keyframes cb-skeleton-pulse rule cycles opacity 1 → 0.55 → 1 over 1.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.
  4. 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

PropTypeDefaultDescription
shape'rect' | 'circle' | 'text''rect'Visual shape of the placeholder. circle forces width = height; text defaults height to 0.75rem.
widthnumber | string'100%'CSS width. Numbers → pixels, strings pass through. Ignored for circle (matches height).
heightnumber | stringshape defaultCSS height. Numbers → pixels, strings pass through. Defaults: rect1rem, circle2.5rem, text0.75rem.
classNamestringMerged 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 parent aria-live region with an accessible label ("Loading article", "Loading avatars") so assistive tech announces what is loading, not the geometry of each placeholder.
  • prefers-reduced-motion: reduce short-circuits the pulse via a CSS-level @media gate — no JS branching, no flash of static-then-animated.
  • Color contrast: placeholders use --cb-bg-muted against 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 a shape enum + explicit width/height, swaps Tailwind's animate-pulse for a project-token-aware @keyframes cb-skeleton-pulse rule, and routes consumers to PageSkeleton for the layout-shaped variants.