Browse docs
Browse docs
Visual placeholder shown while data, images, or components are
loading. Three shapes — text, rectangle, circle — and three
animations — pulse (default), wave, none.
import { Skeleton } from '@dashforge/tw';
<Skeleton variant="text" width="200px" />
<Skeleton variant="rectangle" width="100%" height="120px" />
<Skeleton variant="circle" width="40px" />Use Skeleton when a real layout shape is known but the content inside is still being fetched. The skeleton occupies the same space the eventual content will occupy, so the page doesn't re-flow when data arrives.
Use a plain spinner instead when the layout itself is unknown (e.g. a modal that's still computing its size, or a page that's still routing).
<Skeleton variant="text" width="240px" /> // h-[1em], w-full default
<Skeleton variant="rectangle" width="240px" height="80px" />
<Skeleton variant="circle" width="48px" /> // height defaults to widthvariant | Default shape |
|---|---|
text (default) | h-[1em], w-full — rounded corners — for typical line-of-text placeholders |
rectangle | rounded-md, w-full, h-[100px] — for blocks (image placeholders, cards) |
circle | rounded-full, w-10 h-10 — height defaults to width automatically — for avatars |
<Skeleton variant="rectangle" width="200px" height="40px" animation="pulse" />
<Skeleton variant="rectangle" width="200px" height="40px" animation="wave" />
<Skeleton variant="rectangle" width="200px" height="40px" animation="none" />animation | Effect |
|---|---|
pulse (default) | Opacity 1.0 ↔ 0.5 every 2s. Tailwind's built-in animate-pulse. |
wave | A subtle gradient slides horizontally across the surface (MUI-style "wave"). |
none | No motion. Useful when an outer container is already animated, or to test reduced-motion behavior. |
All animations are automatically suppressed when the user has set
prefers-reduced-motion: reduce in their OS — no extra config
needed.
<div className="flex gap-3 p-4 rounded-lg border">
<Skeleton variant="circle" width="48px" />
<div className="flex flex-col gap-2 flex-1">
<Skeleton variant="text" width="60%" />
<Skeleton variant="text" width="40%" />
<Skeleton variant="rectangle" width="100%" height="60px" />
</div>
</div>Each <Skeleton> is a simple <span aria-hidden="true"> with
inline width / height styles — compose them however the final
layout will look.
Skeleton follows the same dual-override pattern as every other
@dashforge/tw component — see the
Customization guide for the
full decision tree.
sx — outer wrapper<Skeleton variant="rectangle" sx="bg-amber-200 dark:bg-amber-900" />sx is a string of Tailwind utilities, appended after the variant
classes and merged via tailwind-merge. The example above
overrides the default bg-neutral-200 / dark:bg-neutral-800 color
with an amber tint.
slotProps.root<Skeleton
slotProps={{ root: { className: 'transition-opacity duration-500' } }}
/>Skeleton has a single slot (root) because it's an atomic
single-element component. For multi-element placeholders, compose
several <Skeleton>s.
Skeleton renders as a <span> with:
aria-hidden="true"role="presentation"Screen readers skip the skeleton entirely — they shouldn't
announce loading shapes. The surrounding container is responsible
for announcing loading state when relevant, via either
aria-busy="true" on the wrapper or an aria-live region with a
"Loading…" message.
Example pattern for a list:
<ul aria-busy={isLoading}>
{isLoading
? Array.from({ length: 5 }).map((_, i) => (
<li key={i}>
<Skeleton variant="text" width="80%" />
</li>
))
: items.map((item) => <li key={item.id}>{item.name}</li>)}
</ul>The aria-busy flag tells assistive tech "this region is loading,
don't try to read it yet". Once isLoading flips to false, the
content takes over and is announced normally.
If the user has prefers-reduced-motion: reduce set, both the
pulse and wave animations are automatically suppressed — the
skeleton becomes a static grey box. No work required from you.
This is part of the WCAG 2.3.3 (Animation from Interactions) compliance baseline for the entire library.
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | 'text' | 'rectangle' | 'circle' | 'text' | Shape preset |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | Always reduced-motion safe |
width | string | 100% (text/rectangle), 40px (circle) | Any CSS length |
height | string | 1em (text), 100px (rectangle), matches width (circle) | Any CSS length |
sx | string | — | Tailwind utility classes, wins over variant via tailwind-merge |
slotProps.root | { className?: string } | — | Single-slot override |
sx vs slotProps decision tree