Page Skeleton

A full-page skeleton — a title placeholder above a paragraph block above a grid of card placeholders. It claims the shape of the page before any data arrives so the layout doesn't visibly snap into place once content loads. Each placeholder shimmers with a soft pulse (suppressed under reduced motion).

Customize
Paragraph lines
3
Grid items
6
Grid columns
3

Installation

npx shadcn@latest add https://craftbits.dev/r/page-skeleton.json

Usage

import { PageSkeleton } from "@craft-bits/core";
 
<PageSkeleton />

Custom shape:

<PageSkeleton showTitle={false} lines={4} gridItems={4} gridCols={2} />

Understanding the component

  1. Title placeholder. A h-9 bar at 2/3 width that anchors the top of the page where an <h1> would sit. Toggle off with showTitle={false} when the route doesn't have one.
  2. Paragraph lines. A stack of h-3 rounded bars spaced by gap-3. The last line is rendered at 60% width so the block reads as natural prose instead of a uniform slab. Count is clamped to [0, 12].
  3. Grid of cards. A responsive grid of h-28 card placeholders. Column count maps to a responsive Tailwind grid — gridCols={3} becomes grid-cols-2 sm:grid-cols-3, etc. Set gridItems={0} to hide the grid for routes that don't have one. Count is clamped to [0, 24].
  4. Staggered enter. Each placeholder fades and lifts in keyed to its row/section index. The total entrance settles in under 400ms on a typical page; the shimmer takes over from there.
  5. Pulse. Every placeholder cycles opacity 0.5 → 0.85 → 0.5 over 1.6s on a shared rhythm so the page reads as a single living surface, not a constellation of unrelated blinks.
  6. Reduced motion. When prefers-reduced-motion: reduce is set, the entrance lift and the opacity pulse are both suppressed; the placeholders render as a static muted layout.

Props

PropTypeDefaultDescription
labelstring'Loading page'Accessible name announced to screen readers via aria-label.
showTitlebooleantrueRender the title placeholder bar above the paragraph block.
linesnumber3Number of paragraph placeholder lines, clamped to [0, 12]. The last line is rendered at 60% width.
gridItemsnumber6Number of card placeholders in the grid section, clamped to [0, 24]. Set to 0 to hide the grid.
gridColsnumber3Number of grid columns (mapped to a responsive Tailwind grid), clamped to [1, 6].
maxWidthClassNamestring'max-w-3xl'Tailwind max-width utility applied to the inner content column.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • Renders as a <div role="status" aria-busy="true" aria-live="polite"> with an aria-label derived from label, so assistive tech announces both the loading state and a description of what's loading.
  • All placeholder bars and cards are aria-hidden="true" — screen readers read the status's accessible name, not the bar count.
  • prefers-reduced-motion: reduce short-circuits both the entrance lift and the opacity pulse — the placeholders render as a static muted layout, no JS branching beyond the early-return guard.
  • 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.

Credits

  • Extracted from: algoflashcards (src/platform/ui/PageSkeleton.tsx). The source was a thin re-export of a Suspense-fallback variant inside AppLoadingScreen that rendered four track-coloured bars keyed to the project's skill-tree taxonomy. The library version generalises it to a content-shaped page skeleton (title + paragraph + grid) reusable across any product, with a single accent-neutral palette and reduced-motion handling.