Skeleton React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React skeleton loader shaped like the content on its way: a shimmering block, text lines and presets for cards, lists and tables.
Nothing to press: blocks shimmer or pulse while data loads, and the real content fades in the moment loading turns off. Tap Reload in the playground to replay a 1.4s request and watch the cards swap without the layout moving. Wrap real content in a Skeleton and the block takes its exact size and corners; the wrapped content is inert while loading, so Tab skips it until it arrives. On a phone the table preset turns into one card per row. Screen readers hear each preset's label once, never every block, and reduced motion keeps every block still.
Skeleton is a free React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add skeleton, together with UIProvider and Motion presets and the npm package motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add skeletonUsage
Import Skeleton from @/components/ui/skeleton.
import { Skeleton, SkeletonCard } from "@/components/ui/skeleton"
type Listing = { title: string; city: string; photo: string }
export function ListingCard({ listing }: { listing?: Listing }) {
if (!listing) return <SkeletonCard label="Loading the listing" />
return (
<article className="flex flex-col gap-3">
<img src={listing.photo} alt="" className="aspect-[16/10] w-full rounded-[10px] object-cover" />
<h3 className="text-[15px] font-semibold">{listing.title}</h3>
<p className="text-[13px] text-muted-foreground">{listing.city}</p>
</article>
)
}
export function Seller({ loading }: { loading: boolean }) {
return (
<div className="flex items-center gap-3">
<Skeleton shape="circle" loading={loading}>
<img src="/ana-pop.jpg" alt="Emma Carter" className="size-10 rounded-full" />
</Skeleton>
<Skeleton shape="line" loading={loading}>
<p className="text-sm font-medium">Emma Carter</p>
</Skeleton>
</div>
)
}Props
shapedefault"rect""rect" | "circle" | "line"rect: a block with radius (full width, 96px tall until you size it). circle: a round block, 40px unless width says otherwise. line: a 14px text line.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corners of a rect: 0 / 6 / 10 / 16px or a pill. Circles and lines are always round. A rect wrapping real content copies the corners of what it wraps when that has any (a button, a rounded photo).
animationdefault"shimmer""shimmer" | "pulse" | "none"shimmer: a soft highlight sweeps across. pulse: the block fades in and out. none: static. Under reduced motion every block stays still.
colordefault""stringAny CSS color for the block. Leave empty for the 5% foreground tint, which works on every surface in both themes. Set it on colored surfaces, e.g. a white tint on a brand banner.
highlightdefault""stringAny CSS color for the shimmer band. Leave empty for a 7% foreground tint.
speeddefault1numberPlayback multiplier for shimmer and pulse: 0.5 is half speed, 2 twice as fast. One shimmer sweep takes 1.6s at 1, a pulse 2s. Clamped to 0.25..4.
loadingdefaulttruebooleanfalse renders the children instead, fading them in. Content that is already there on the first render (the server's too) shows without a fade.
childrenReactNodeThe real content. While loading it sits invisible and inert inside the block, so the block takes its exact size (a rect its corners too, a line draws a bar 70% as tall as the text box, like the presets); once loaded it shows in a wrapper that fades in. className lands on both, so pass layout classes like w-full or flex-1 once.
widthdefault""number | stringpx as a number (or a number-like string such as "120"), any other CSS length as a string. rect and line fill the row by default; a circle takes its size from width, or from height when width is missing.
heightdefault""number | stringpx as a number (or a number-like string), any other CSS length as a string. A rect is 96px tall by default, or follows an aspect-* class. Ignored by circles, which stay round.
classNamestringExtra classes for the block, merged last: size it with h-4 w-32, size-12, aspect-video.
Parts
SkeletonText
componentA column of lines: 3 by default, the last at 60%. size sm / md / lg makes 10 / 14 / 18px lines (text-xs, text-sm, text-[15px]). lines={0} renders nothing. Props: { lines?: number; lastLineWidth?: number | string; size?: "sm" | "md" | "lg"; animation; speed; color; highlight; classNames?: { line } }.
SkeletonCard
componentA media card: a 16:10 block on top (media, true; mediaAspect takes any CSS aspect ratio, "1" for square thumbnails), a title and meta lines (lines, 2), a price line and a round action (footer, true). role="status" with a hidden label ("Loading"). Every preset also takes animation, speed, color and highlight. Props: { media?: boolean; mediaAspect?: string; lines?: number; footer?: boolean; radius?; label?: string; classNames?: { media, body, footer } }.
SkeletonList
componentList group rows (56px, px-4): a 40px avatar, a title and a subtitle, a button-sized action, hairlines between rows. 4 rows by default. classNames.avatar="rounded-[10px]" makes square thumbnails. Props: { rows?: number; avatar?: boolean; action?: boolean; dividers?: boolean; label?: string; classNames?: { row, avatar, action } }.
SkeletonTable
componentA table-shaped grid from 768px up (a header and 48px rows, the first column wider), one card per row below: a title line and two label / value pairs. Pure CSS, right on the first paint. 5 rows, 4 columns. Props: { rows?: number; columns?: number; header?: boolean; label?: string; classNames?: { header, row, card } }.
SkeletonProfile
componentA profile header: a cover the avatar overlaps through a round cut-out, name and meta lines, and a row of stats (3). avatarSize is in px (80). Props: { cover?: boolean; stats?: number; avatarSize?: number; radius?; label?: string; classNames?: { cover, avatar, stats } }.
Examples
Primitives
A circle and two lines for a list row, then a rect in each radius.
Text
SkeletonText with 1, 3 and 5 lines at lg, md and sm. The last line stops short, like real text.
List, then the real rows
SkeletonList in a list group swaps to the real rows after a fake 1.4s request. The rows share its rhythm, so nothing moves.
Table on every screen
A table-shaped grid from 768px up, one card per row on phones. Pure CSS, right on the first paint.
Profile
The avatar overlaps the cover through a round cut-out, so the two tints never stack into a darker patch.
Auto-size
Wrap the real content: while loading it sits invisible inside the block, which takes its exact size and corners.
Shimmer, pulse, none
A sweeping highlight, a slow fade, or a still block. Reduced motion always gets the still one.
On a colored surface
color and highlight take any CSS color, here white tints on a brand banner.
Files
- components/ui/skeleton.tsx
FAQ
Is the React Skeleton component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Skeleton?
Run npx wingo-ui@latest add skeleton. It also adds the Wingo UI items it builds on (UIProvider and Motion presets) and the npm package motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/skeleton.json.
How does Skeleton behave on phones?
On a phone the table preset turns into one card per row.
See also
Used in
More React feedback components
Articles using this component
- React Empty State Component: First Use, No Results, Errors
Component guides8 min read
- React Skeleton Loader: Real Shapes, No Layout Shift
Component guides9 min read


