Skeleton
Reserve space while content is still loading.
loadingstatus
Preview
Skeleton preview
Live · keyboard ready
When to use
- Loading states for content whose shape is known, like a profile or comment.
- Swapping a placeholder into real content with a crossfade and height spring.
When not to use
- Use progress when you can report a percentage.
- Use empty-state when loading finished and there is nothing to show.
- Use text-shimmer for an AI thinking or status line.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "skeleton",
"type": "registry:ui",
"title": "Skeleton",
"description": "Reserve space while content is still loading.",
"categories": [
"feedback"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/skeleton/skeleton.tsx",
"type": "registry:component",
"target": "@components/manicat/skeleton/skeleton.tsx"
},
{
"path": "registry/components/skeleton/skeleton.module.css",
"type": "registry:component",
"target": "@components/manicat/skeleton/skeleton.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/skeleton",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/skeleton",
"markdown": "https://uiarc.dev/components/skeleton/markdown",
"tags": [
"loading",
"status"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { Skeleton } from "@/components/manicat/skeleton/skeleton";
export function SkeletonExample() {
return (
<div className="grid w-full max-w-md gap-3">
<Skeleton avatar lines={3} label="Loading profile" />
<Skeleton lines={2} />
</div>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Loading a profile The placeholder keeps the final layout, then crossfades into the real content.
Loading a profile
The placeholder keeps the final layout, then crossfades into the real content. preview
Live · keyboard ready
loading-a-profile-the-placeholder-keeps-the-final-layout-then-crossfades-into-the-real-content-.tsxtsx
"use client";
import { useState } from "react";
import { Button } from "@/components/manicat/button/button";
import { Skeleton } from "@/components/manicat/skeleton/skeleton";
import { Avatar } from "@/components/manicat/avatar/avatar";
export function SkeletonProfileExample() {
const [loading, setLoading] = useState(true);
return (
<div className="grid w-full max-w-md gap-4">
<Button size="sm" variant="secondary" onClick={() => setLoading((value) => !value)}>
{loading ? "Finish loading" : "Load again"}
</Button>
<Skeleton loading={loading} avatar lines={3} label="Loading profile">
<div className="flex items-start gap-3">
<Avatar name="Ada Lovelace" />
<div>
<p className="text-sm font-medium text-(color:--foreground)">Ada Lovelace</p>
<p className="text-sm text-(color:--text-muted)">Wrote the first program. Currently leading the analytics team.</p>
</div>
</div>
</Skeleton>
</div>
);
}
Accessibility
- The placeholder is role="status" with aria-busy and an aria-label; its shapes are aria-hidden.
- With children, the wrapper sets aria-busy while loading.
Motion
- Blocks pulse in a staggered wave, each a beat after the one above.
- On load the placeholder fades out, content rises 4px into place, and the height springs from placeholder to content.
- Reduced motion stops the pulse and swaps with short fades and no height animation.
Responsive behavior
- Lines are percentages of the container width, so the placeholder scales with its slot.
- It only draws text lines and an avatar; build custom shapes for grids or media.
Performance
- The pulse is a CSS animation that stops under reduced motion.
- A ResizeObserver springs the height from placeholder to content; avoid hundreds of skeletons at once.
Notes for AI
- Use while fetching content whose shape is known. Use progress when you can report a percentage, and empty-state when there is nothing to show.
- Wrap the real content as children and drive loading to get the crossfade; without children it renders only the placeholder.