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.jsonUsage
import { PageSkeleton } from "@craft-bits/core";
<PageSkeleton />Custom shape:
<PageSkeleton showTitle={false} lines={4} gridItems={4} gridCols={2} />Understanding the component
- Title placeholder. A
h-9bar at 2/3 width that anchors the top of the page where an<h1>would sit. Toggle off withshowTitle={false}when the route doesn't have one. - Paragraph lines. A stack of
h-3rounded bars spaced bygap-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]. - Grid of cards. A responsive grid of
h-28card placeholders. Column count maps to a responsive Tailwind grid —gridCols={3}becomesgrid-cols-2 sm:grid-cols-3, etc. SetgridItems={0}to hide the grid for routes that don't have one. Count is clamped to[0, 24]. - Staggered enter. Each placeholder fades and lifts in keyed to its row/section index. The total entrance settles in under
400mson a typical page; the shimmer takes over from there. - Pulse. Every placeholder cycles opacity
0.5 → 0.85 → 0.5over1.6son a shared rhythm so the page reads as a single living surface, not a constellation of unrelated blinks. - Reduced motion. When
prefers-reduced-motion: reduceis set, the entrance lift and the opacity pulse are both suppressed; the placeholders render as a static muted layout.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Loading page' | Accessible name announced to screen readers via aria-label. |
showTitle | boolean | true | Render the title placeholder bar above the paragraph block. |
lines | number | 3 | Number of paragraph placeholder lines, clamped to [0, 12]. The last line is rendered at 60% width. |
gridItems | number | 6 | Number of card placeholders in the grid section, clamped to [0, 24]. Set to 0 to hide the grid. |
gridCols | number | 3 | Number of grid columns (mapped to a responsive Tailwind grid), clamped to [1, 6]. |
maxWidthClassName | string | 'max-w-3xl' | Tailwind max-width utility applied to the inner content column. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- Renders as a
<div role="status" aria-busy="true" aria-live="polite">with anaria-labelderived fromlabel, 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: reduceshort-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-mutedagainst 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 insideAppLoadingScreenthat 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.