Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/skeleton.json
Installs to @/components/ui/skeleton.
Manual installation
Copy the source into your project:
components/ui/skeleton.tsximport { cn } from '@/lib/utils'
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="skeleton" className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />
}
export { Skeleton }
A skeleton component to display a placeholder loading state for content that is yet to load.
Features
- A plain
div with animate-pulse, rounded-md and the muted background (bg-muted)
- Customizable dimensions and shape via
className
- Composable for complex loading states
data-slot="skeleton" for styling hooks
Usage
import { Skeleton } from "@/components/ui/skeleton"
export function LoadingProfile() {
return (
<div className="flex items-center gap-4">
<Skeleton className="size-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)
}
Examples
Default
A case handler loading: an avatar circle and two lines of text. The wrapper is marked aria-busy and carries a visually hidden status message.
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonDemo() {
return (
<div className="flex w-sm max-w-full items-center gap-4" aria-busy="true">
<Skeleton className="size-12 shrink-0 rounded-full" />
<div className="flex w-full flex-col gap-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
<span className="sr-only">Loading case handler…</span>
</div>
)
}
Card
A building permit card while it loads, keeping the header, preview, body text and footer actions in place so nothing jumps when the content arrives.
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonCaseCard() {
return (
<Card className="w-full max-w-sm" aria-busy="true">
<CardHeader>
<Skeleton className="h-5 w-2/3" />
<Skeleton className="h-4 w-1/3" />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Skeleton className="aspect-video w-full rounded-lg" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-2/3" />
</div>
</CardContent>
<CardFooter className="justify-between">
<Skeleton className="h-5 w-20 rounded-full" />
<Skeleton className="h-10 w-24" />
</CardFooter>
<span className="sr-only">Loading building permit…</span>
</Card>
)
}
List
A case list with avatars, titles, reference lines and status badges.
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonCaseList() {
return (
<div className="w-full max-w-lg rounded-xl border" aria-busy="true">
<div className="flex items-center justify-between border-b px-4 py-3">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-10 w-28" />
</div>
<ul className="divide-y">
{['a', 'b', 'c', 'd'].map((key, index) => (
<li key={key} className="flex items-center gap-3 px-4 py-3">
<Skeleton className="size-8 shrink-0 rounded-full" />
<div className="flex flex-1 flex-col gap-1.5">
<Skeleton className={index % 2 ? 'h-4 w-1/2' : 'h-4 w-2/3'} />
<Skeleton className="h-3 w-1/3" />
</div>
<Skeleton className="h-5 w-16 rounded-full" />
</li>
))}
</ul>
<span className="sr-only">Loading cases…</span>
</div>
)
}
An e-service application form waiting for prefilled resident data.
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonForm() {
return (
<div className="flex w-full max-w-sm flex-col gap-6" aria-busy="true">
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-4/5" />
</div>
{['name', 'address', 'email'].map((key) => (
<div key={key} className="flex flex-col gap-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
))}
<div className="flex justify-end gap-2">
<Skeleton className="h-10 w-20" />
<Skeleton className="h-10 w-28" />
</div>
<span className="sr-only">Loading application form…</span>
</div>
)
}
Stats
Dashboard tiles for open, due and closed cases.
import { Card, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonStats() {
return (
<div className="grid w-full max-w-2xl grid-cols-1 gap-4 sm:grid-cols-3" aria-busy="true">
{['open', 'due', 'closed'].map((key) => (
<Card key={key} size="sm">
<CardContent className="flex flex-col gap-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-16" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
<span className="sr-only">Loading case statistics…</span>
</div>
)
}
Anatomy
<Skeleton className="h-4 w-[250px]" />
Props
Skeleton
The root component that renders a skeleton placeholder.
Accessibility
The Skeleton is purely visual and sets no ARIA attributes itself. To communicate loading to assistive technology:
- Mark the loading region with
aria-busy="true" and restore it when content arrives
- Optionally add a visually hidden status message (e.g.
<span className="sr-only">Loading...</span>)
- Respect reduced motion, e.g.
motion-reduce:animate-none
Best Practices
- Match skeleton dimensions to actual content
- Use appropriate spacing and layout
- Keep animations subtle and not distracting
- Group related skeleton elements
- Consider mobile responsiveness
Customization
Custom Dimensions
Adjust the width and height using Tailwind classes:
<Skeleton className="h-8 w-32" />
Custom Shape
Modify the border radius using Tailwind classes:
<Skeleton className="h-12 w-12 rounded-full" /> // Circle
<Skeleton className="h-24 w-full rounded-xl" /> // Rounded rectangle
Custom Animation
The default pulse animation can be customized through Tailwind:
<Skeleton className="animate-[pulse_2s_ease-in-out_infinite]" />
- Progress - For determinate loading states
- Spinner - For inline loading indicators
- Card - For content layouts that might need skeleton states