Skeleton
A loading state skeleton for your components. Built with Base UI and Tailwind CSS. Copy-paste ready.
"use client";
import { UserRoundPlusIcon, UsersRoundIcon } from "lucide-react";
import { useEffect, useState } from "react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
const users = [
{
delay: 3000,
fallback: "SJ",
followers: "15k",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&dpr=2&q=80",
name: "Sarah Johnson",
role: "Design Engineer",
},
{
delay: 4000,
fallback: "MA",
followers: "8k",
image:
"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=80&h=80&dpr=2&q=80",
name: "Mark Bennett Andersson",
role: "Product Designer",
},
{
delay: 3400,
fallback: "AR",
followers: "12k",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&dpr=2&q=80",
name: "Alex Rivera",
role: "UI/UX Designer",
},
];
function UserCard({ delay, user }: { delay: number; user: (typeof users)[0] }) {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setIsLoaded(true);
}, delay);
return () => clearTimeout(timer);
}, [delay]);
if (!isLoaded) {
return <UserCardSkeleton />;
}
return (
<>
<Avatar className="size-10">
<AvatarImage alt={user.name} src={user.image} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<h4 className="line-clamp-1 font-medium text-sm">{user.name}</h4>
<div className="flex items-center gap-3 text-muted-foreground text-xs">
<span className="truncate">{user.role}</span>
<div className="flex min-w-0 items-center gap-1">
<UsersRoundIcon className="size-3 shrink-0" />
<span className="truncate">
{user.followers}
<span className="max-sm:hidden"> followers</span>
</span>
</div>
</div>
</div>
<Button size="xs">
<UserRoundPlusIcon />
Follow
</Button>
</>
);
}
function UserCardSkeleton() {
return (
<>
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-1 flex-col">
<Skeleton className="my-0.5 h-4 max-w-54" />
<div className="flex max-w-54 items-center gap-1">
<Skeleton className="my-0.5 h-4 w-1/2" />
<Skeleton className="my-0.5 h-4 w-1/2" />
</div>
</div>
<Skeleton className="h-7 w-19 sm:h-6 sm:w-17" />
</>
);
}
export default function Particle() {
return (
<div className="flex w-full max-w-92 flex-col gap-6">
{users.map((user) => (
<div className="flex items-center gap-4" key={user.fallback}>
<UserCard delay={user.delay} user={user} />
</div>
))}
</div>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/skeleton
Usage
import { Skeleton } from "@/components/ui/skeleton"<Skeleton className="size-10 rounded-full" />Examples
Skeleton only
A set of unstyled skeleton blocks demonstrating how to compose circles and rectangles to match your layout's shape.
import { Skeleton } from "@/components/ui/skeleton";
export default function Particle() {
return (
<div className="flex w-full max-w-92 items-center gap-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-1 flex-col">
<Skeleton className="my-0.5 h-4 max-w-54" />
<div className="flex max-w-54 items-center gap-1">
<Skeleton className="my-0.5 h-4 w-1/2" />
<Skeleton className="my-0.5 h-4 w-1/2" />
</div>
</div>
<Skeleton className="h-6 w-17" />
</div>
);
}
Card component
A card-shaped skeleton with a rectangular image area, a title line, and two description lines — a common content card placeholder.
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export function Pattern() {
return (
<Card className="w-full max-w-xs">
<CardHeader className="gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-1/2" />
</CardHeader>
<CardContent>
<Skeleton className="aspect-video w-full rounded-md" />
</CardContent>
</Card>
);
}
Dashboard stats row
A row of metric cards each containing a skeleton title and a large number area, used while dashboard data loads.
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export function Pattern() {
return (
<div className="mx-auto grid w-full max-w-lg grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => {
const k = `skeleton-card-${i}`;
return (
<Card key={k}>
<CardHeader className="pb-2">
<Skeleton className="h-3 w-16" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-3 w-20" />
</CardContent>
</Card>
);
})}
</div>
);
}
List with actions
A list skeleton with a circular avatar, two text lines, and a trailing action area — suitable for people lists or activity feeds.
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
export function Pattern() {
return (
<div className="mx-auto w-full max-w-xs">
{/* Skeleton pattern */}
<div className="flex flex-col">
<div className="flex items-center justify-between pb-4">
<Skeleton className="h-5 w-28" />
<Skeleton className="h-8 w-20 rounded-md" />
</div>
<Separator className="opacity-60" />
{Array.from({ length: 3 }).map((_, i) => {
const k = `skeleton-list-item-${i}`;
return (
<div
className="flex items-center gap-3 border-border/60 border-b py-3 last:border-b-0"
key={k}
>
<Skeleton className="size-9 shrink-0 rounded-full" />
<div className="flex flex-1 flex-col gap-1.5">
<Skeleton className="h-3.5 w-32" />
<Skeleton className="h-3 w-48" />
</div>
<Skeleton className="h-7 w-16 rounded-md" />
</div>
);
})}
</div>
</div>
);
}
Blog Article
A full article skeleton with a hero image placeholder, tag pills, a wide title line, author row, and multiple body paragraphs.
import { Skeleton } from "@/components/ui/skeleton";
export function Pattern() {
return (
<div className="mx-auto w-full max-w-xl space-y-6">
<Skeleton className="aspect-[2/1] w-full rounded-xl" />
<div className="space-y-3">
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-16 rounded-full" />
<Skeleton className="h-5 w-20 rounded-full" />
</div>
<Skeleton className="h-7 w-full" />
<Skeleton className="h-7 w-4/5" />
</div>
<div className="flex items-center gap-3">
<Skeleton className="size-9 rounded-full" />
<div className="space-y-1.5">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>
</div>
);
}
Data Table
A table skeleton with a header row, six data rows each containing an avatar, badge, text cells, and a trailing button, plus a pagination footer.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { Skeleton } from "@/components/ui/skeleton";
const colWidths = ["w-32", "w-20", "w-24", "w-16", "w-14"];
export function Pattern() {
return (
<div className="w-full max-w-2xl overflow-hidden rounded-xl border">
<div className="flex items-center gap-4 border-b bg-muted/40 px-4 py-3">
{colWidths.map((w, i) => (
<Skeleton className={`h-3.5 ${w} shrink-0`} key={i} />
))}
</div>
{Array.from({ length: 6 }).map((_, row) => (
<div
className="flex items-center gap-4 border-b px-4 py-3.5 last:border-b-0"
key={row}
>
<div className="flex w-32 shrink-0 items-center gap-2.5">
<Skeleton className="size-7 rounded-full" />
<Skeleton className="h-3.5 flex-1" />
</div>
<Skeleton className={"h-5 w-14 shrink-0 rounded-full"} />
<Skeleton className="h-3.5 w-24 shrink-0" />
<Skeleton className="h-3.5 w-16 shrink-0" />
<Skeleton className="h-6 w-14 shrink-0 rounded-md" />
</div>
))}
<div className="flex items-center justify-between border-t bg-muted/30 px-4 py-3">
<Skeleton className="h-3.5 w-32" />
<div className="flex items-center gap-2">
<Skeleton className="h-7 w-7 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
</div>
</div>
);
}
Sidebar Dashboard
A two-panel layout skeleton: a sidebar with nav items and a main area with a stats grid and list rows — mirrors a full dashboard loading state.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { Skeleton } from "@/components/ui/skeleton";
export function Pattern() {
return (
<div className="flex h-80 w-full max-w-2xl overflow-hidden rounded-xl border">
<div className="flex w-48 shrink-0 flex-col gap-1 border-r bg-muted/30 p-3">
<div className="mb-2 flex items-center gap-2 px-1 py-1">
<Skeleton className="size-6 rounded-md" />
<Skeleton className="h-4 w-20" />
</div>
{[60, 44, 52, 36].map((w, i) => (
<div
className="flex items-center gap-2 rounded-md px-2 py-1.5"
key={i}
>
<Skeleton className="size-4 rounded-sm" />
<Skeleton
className={`h-3.5 w-${w === 60 ? "full" : `[${w}%]`}`}
style={{ width: `${w}%` }}
/>
</div>
))}
<div className="mt-3 border-t pt-3">
{[48, 56].map((w, i) => (
<div
className="flex items-center gap-2 rounded-md px-2 py-1.5"
key={i}
>
<Skeleton className="size-4 rounded-sm" />
<Skeleton className="h-3.5" style={{ width: `${w}%` }} />
</div>
))}
</div>
<div className="mt-auto flex items-center gap-2 px-2 py-1.5">
<Skeleton className="size-7 rounded-full" />
<div className="space-y-1">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-2.5 w-16" />
</div>
</div>
</div>
<div className="flex flex-1 flex-col">
<div className="flex items-center justify-between border-b px-5 py-3">
<Skeleton className="h-5 w-32" />
<div className="flex items-center gap-2">
<Skeleton className="h-7 w-20 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
</div>
<div className="flex-1 space-y-4 p-5">
<div className="grid grid-cols-3 gap-3">
{Array.from({ length: 3 }).map((_, i) => (
<div className="space-y-2 rounded-lg border p-3" key={i}>
<Skeleton className="h-3 w-16" />
<Skeleton className="h-6 w-20" />
<Skeleton className="h-3 w-24" />
</div>
))}
</div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<div
className="flex items-center gap-3 rounded-lg border px-3 py-2.5"
key={i}
>
<Skeleton className="size-8 rounded-md" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-3.5 w-40" />
<Skeleton className="h-3 w-28" />
</div>
<Skeleton className="h-5 w-14 rounded-full" />
</div>
))}
</div>
</div>
</div>
</div>
);
}
Chat Thread
Alternating left and right message bubble skeletons with an avatar on the left side and an input bar at the bottom — suitable for chat or messaging interfaces.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { Skeleton } from "@/components/ui/skeleton";
const messages = [
{ lines: [72, 56], self: false },
{ lines: [88], self: true },
{ lines: [64, 80, 48], self: false },
{ lines: [76, 60], self: true },
{ lines: [52], self: false },
];
export function Pattern() {
return (
<div className="flex w-full max-w-sm flex-col overflow-hidden rounded-xl border">
<div className="flex items-center gap-3 border-b px-4 py-3">
<Skeleton className="size-8 rounded-full" />
<div className="space-y-1.5">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<div className="flex flex-1 flex-col gap-4 px-4 py-4">
{messages.map((msg, i) => (
<div
className={`flex items-end gap-2 ${msg.self ? "flex-row-reverse" : ""}`}
key={i}
>
{!msg.self && (
<Skeleton className="mb-0.5 size-7 shrink-0 rounded-full" />
)}
<div
className={`flex max-w-[75%] flex-col gap-1 ${msg.self ? "items-end" : "items-start"}`}
>
{msg.lines.map((w, j) => (
<Skeleton
className={`h-8 rounded-2xl ${
msg.self
? j === 0
? "rounded-tr-sm"
: j === msg.lines.length - 1
? "rounded-br-sm"
: ""
: j === 0
? "rounded-tl-sm"
: j === msg.lines.length - 1
? "rounded-bl-sm"
: ""
}`}
key={j}
style={{ width: `${w}%` }}
/>
))}
</div>
</div>
))}
</div>
<div className="flex items-center gap-2 border-t px-3 py-2.5">
<Skeleton className="h-9 flex-1 rounded-full" />
<Skeleton className="size-9 shrink-0 rounded-full" />
</div>
</div>
);
}
Settings Page
A settings page skeleton with a header, a tab bar, and two settings sections each containing toggle and button rows.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { Skeleton } from "@/components/ui/skeleton";
function SettingsSection({
rows,
titleWidth,
}: {
rows: number;
titleWidth: string;
}) {
return (
<div className="space-y-4">
<div className="space-y-1">
<Skeleton className={`h-4 ${titleWidth}`} />
<Skeleton className="h-3 w-48" />
</div>
<div className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
<div
className="flex items-center justify-between gap-4 rounded-lg border px-4 py-3"
key={i}
>
<div className="space-y-1.5">
<Skeleton className="h-3.5 w-32" />
<Skeleton className="h-3 w-52" />
</div>
{i % 3 === 2 ? (
<Skeleton className="h-8 w-20 shrink-0 rounded-md" />
) : (
<Skeleton className="h-5 w-9 shrink-0 rounded-full" />
)}
</div>
))}
</div>
</div>
);
}
export function Pattern() {
return (
<div className="mx-auto w-full max-w-lg space-y-8">
<div className="flex items-center justify-between">
<div className="space-y-1.5">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-3.5 w-48" />
</div>
<Skeleton className="h-9 w-24 rounded-md" />
</div>
<div className="flex gap-1">
{[60, 80, 56, 72].map((w, i) => (
<Skeleton
className="h-8 rounded-md"
key={i}
style={{ width: `${w}px` }}
/>
))}
</div>
<SettingsSection rows={3} titleWidth="w-36" />
<SettingsSection rows={2} titleWidth="w-28" />
</div>
);
}
Product Grid
A 2×2 grid of e-commerce product card skeletons, each with a square image placeholder, title, star rating, price, and an add-to-cart button.
import { Skeleton } from "@/components/ui/skeleton";
export default function Particle() {
return (
<div className="grid w-full max-w-lg grid-cols-2 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div className="overflow-hidden rounded-xl border" key={String(i)}>
<Skeleton className="aspect-square w-full rounded-none" />
<div className="space-y-2 p-3">
<Skeleton className="h-4 w-3/4" />
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, j) => (
<Skeleton className="size-3 rounded-sm" key={String(j)} />
))}
<Skeleton className="ms-1 h-3 w-8" />
</div>
<div className="flex items-center justify-between pt-1">
<Skeleton className="h-5 w-16" />
<Skeleton className="h-7 w-20 rounded-md" />
</div>
</div>
</div>
))}
</div>
);
}
Form Fields
A multi-field form skeleton with a title/description header, a two-column field grid, a full-width textarea, and a footer action row.
import { Skeleton } from "@/components/ui/skeleton";
const fields = [
{ wide: true },
{ wide: true },
{ wide: false },
{ wide: false },
{ wide: true },
];
export default function Particle() {
return (
<div className="w-full max-w-sm space-y-5">
<div className="space-y-1.5">
<Skeleton className="h-6 w-36" />
<Skeleton className="h-4 w-52" />
</div>
<div className="grid grid-cols-2 gap-4">
{fields.map(({ wide }, i) => (
<div
className={`space-y-1.5 ${wide ? "col-span-2" : ""}`}
key={String(i)}
>
<Skeleton className="h-3.5 w-20" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
))}
</div>
<div className="space-y-1.5">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<Skeleton className="h-9 w-20 rounded-lg" />
<Skeleton className="h-9 w-28 rounded-lg" />
</div>
</div>
);
}
Email Inbox
An inbox list skeleton with a header toolbar and rows showing a sender avatar, name, subject, preview line, and timestamp — unread rows subtly highlighted.
import { Skeleton } from "@/components/ui/skeleton";
const rows = [
{ preview: 80, read: false, subject: 56 },
{ preview: 72, read: false, subject: 48 },
{ preview: 88, read: true, subject: 64 },
{ preview: 60, read: true, subject: 40 },
{ preview: 76, read: true, subject: 52 },
{ preview: 68, read: true, subject: 44 },
];
export default function Particle() {
return (
<div className="w-full max-w-lg overflow-hidden rounded-xl border">
<div className="flex items-center justify-between border-b bg-muted/30 px-4 py-3">
<Skeleton className="h-4 w-12" />
<div className="flex items-center gap-2">
<Skeleton className="h-7 w-20 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
</div>
{rows.map(({ preview, read, subject }, i) => (
<div
className={`flex items-start gap-3 border-b px-4 py-3 last:border-b-0 ${!read ? "bg-accent/20" : ""}`}
key={String(i)}
>
<Skeleton className="mt-0.5 size-8 shrink-0 rounded-full" />
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-center justify-between gap-2">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-12 shrink-0" />
</div>
<Skeleton className={"h-3.5"} style={{ width: `${subject}%` }} />
<Skeleton className={"h-3"} style={{ width: `${preview}%` }} />
</div>
{!read && (
<Skeleton className="mt-1.5 size-2 shrink-0 rounded-full" />
)}
</div>
))}
</div>
);
}
Kanban Board
Three columns (To Do, In Progress, Done) each containing stacked card skeletons with title, assignee avatars, and a status pill — plus an add-card button.
import { Skeleton } from "@/components/ui/skeleton";
const columns = [
{ cards: 3, label: "To Do" },
{ cards: 2, label: "In Progress" },
{ cards: 4, label: "Done" },
];
export default function Particle() {
return (
<div className="flex w-full max-w-2xl gap-4 overflow-x-auto pb-2">
{columns.map(({ cards, label }) => (
<div
className="w-60 shrink-0 space-y-3 rounded-xl border bg-muted/30 p-3"
key={label}
>
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-5 w-5 rounded-full" />
</div>
{Array.from({ length: cards }).map((_, i) => (
<div
className="space-y-2.5 rounded-lg border bg-background p-3"
key={String(i)}
>
<Skeleton className="h-3.5 w-4/5" />
<Skeleton className="h-3 w-3/5" />
<div className="flex items-center justify-between pt-1">
<div className="flex -space-x-1.5">
{Array.from({ length: 2 }).map((_, j) => (
<Skeleton
className="size-5 rounded-full ring-2 ring-background"
key={String(j)}
/>
))}
</div>
<Skeleton className="h-4 w-12 rounded-full" />
</div>
</div>
))}
<Skeleton className="h-7 w-full rounded-md" />
</div>
))}
</div>
);
}
Profile Hero Page
A social profile skeleton: a banner image, an overlapping circular avatar, name/handle, bio text, a stats bar, and a six-item post image grid.
import { Skeleton } from "@/components/ui/skeleton";
export default function Particle() {
return (
<div className="w-full max-w-lg overflow-hidden rounded-xl border">
<Skeleton className="h-28 w-full rounded-none" />
<div className="px-5 pb-5">
<div className="-mt-8 mb-4 flex items-end justify-between gap-3">
<Skeleton className="size-16 rounded-full ring-4 ring-background" />
<Skeleton className="h-8 w-24 rounded-lg" />
</div>
<div className="space-y-1.5">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-28" />
</div>
<div className="mt-3 space-y-1.5">
<Skeleton className="h-3.5 w-full" />
<Skeleton className="h-3.5 w-5/6" />
<Skeleton className="h-3.5 w-4/5" />
</div>
<div className="mt-4 flex items-center gap-4 border-y py-3">
{Array.from({ length: 3 }).map((_, i) => (
<div className="flex items-center gap-1.5" key={String(i)}>
<Skeleton className="h-4 w-10" />
<Skeleton className="h-3.5 w-16" />
</div>
))}
</div>
<div className="mt-4 grid grid-cols-3 gap-2">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton
className="aspect-square w-full rounded-lg"
key={String(i)}
/>
))}
</div>
</div>
</div>
);
}

