Feedback
Skeleton
A loading-placeholder primitive — line, circle and rect shapes with pulse or shimmer motion — that mirrors the layout it stands in for.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/skeleton.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Skeleton" component using
class-variance-authority for its variant/shape matrix. No other dependencies.
Contract
- Export a forwardRef div, Skeleton, extending React.HTMLAttributes<HTMLDivElement>.
- Props: variant?: "pulse" | "shimmer" (default "pulse"); shape?: "line" |
"circle" | "rect" (default "line"); className merged last via cn() — this is
the primary way callers size an instance (width/height utilities).
- shape="line" ships its own h-4 height and rounded corners, but no width — a
bare block-level div already stretches to its container's width, so a plain
<Skeleton /> renders a full-width text-line placeholder; pass a w-* utility
to narrow it. shape="circle" (rounded-full) and shape="rect" (rounded-lg)
intentionally ship no default size — the caller must supply both width and
height via className (e.g. size-10 for an avatar, h-32 w-full for a cover).
Behavior
- variant="pulse" applies the built-in animate-pulse utility to the whole
block — a uniform opacity breathing loop.
- variant="shimmer" instead renders an absolutely-positioned overlay span
inside the block (the block itself is relative + overflow-hidden): a
gradient from-transparent via-foreground/10 to-transparent, animated via a
hoisted <style> keyframe that translates it from -100% to 100% on an
infinite ease-in-out loop — a light sweep passing left to right across the
placeholder.
- Reduced motion: both animations stop under prefers-reduced-motion (pulse
via motion-reduce:animate-none, the shimmer overlay via motion-reduce:hidden
so it stops rendering entirely) — but the static bg-muted block itself never
disappears, so the placeholder still communicates "content goes here" with
motion off.
- The component is purely decorative: it carries aria-hidden="true" always.
It does not itself announce a loading state — the container that swaps this
skeleton in for real content is responsible for aria-busy="true" (and
removing it once real content mounts) so assistive tech gets the loading
signal from one place instead of from every individual placeholder block.
- No hooks, no client-only APIs — this stays a plain (non-"use client")
component; it renders fine as a server component.
Rendering & styling
- Semantic tokens only: bg-muted for the block, foreground/10 for the shimmer
gradient's midpoint. No hardcoded colors.
- cn() merges className onto the variant/shape classes so callers can safely
override width, height and radius.
Customization levers
- Shape: add more cva shape entries (e.g. a "badge" pill shape) alongside
line/circle/rect — radius is entirely owned by the shape variant.
- Motion strength: swap animate-pulse for a custom opacity range, or tune the
shimmer's animation-duration and via-foreground opacity for a
subtler/bolder sweep.
- Composition: since this is a single primitive, layout recipes (a user card,
a list row) are just several Skeleton instances arranged with flex/grid in
the consuming component — mirror the real content's shape exactly so the
transition from placeholder to data reads as a like-for-like swap.Concepts
- Skeleton mirrors layout — the placeholder's shapes (line/circle/rect) are arranged to match the real content's structure, so loading and loaded states occupy the same space and nothing jumps when data arrives.
- Composition as recipe — a "user card" or "list row" skeleton isn't a separate component, it's several Skeleton primitives stacked with the same flex/grid the real content uses.
- Pulse vs shimmer — pulse is a uniform breathing opacity (cheap, ambient); shimmer is a directional light sweep (reads as more "active"); both are purely aesthetic choices over the same shapes.
- Decorative, not announcing — Skeleton itself is aria-hidden; the loading state is announced once by the container's aria-busy, not by every individual placeholder block.
- Motion-off still visible — reduced motion removes both animations but never removes the bg-muted block itself, so the placeholder keeps doing its job (holding the shape) with motion off.
Animated Tooltip
A self-contained hover/focus tooltip with a spring pop-in and a reduced-motion fallback — no Radix or floating-ui required.
Callout
An inline highlight box for docs, forms and explanatory copy — info, success, warning and danger variants with a tinted accent bar and default icon per variant.