A shimmering placeholder in the shape and size of content that is still loading, so the content takes its place without moving anything. Screen readers skip it, so mark the region that is loading with aria-busy while it shows. With reduced motion the shimmer holds still.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/skeleton#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/skeletonIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/skeleton#v1.3.10 ourostack/ouro-ui/skeleton-tests#v1.3.10import { Skeleton } from "@/components/ouro/skeleton/skeleton";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<>
<div aria-busy="true" aria-label="Messages" role="group">
<Skeleton shape="row" count={6} />
</div>
<Skeleton shape="text" size="sm" lines={3} />
<Skeleton shape="circle" size="lg" />
</>Usage
Pick the shape of what is coming: shape="text" with lines and Text's size for a paragraph, shape="circle" with Avatar's size for an avatar, shape="row" with count for a list of Items, shape="card" for Cards, and rect sized with a layout class for an image or a chart. A placeholder stands in for content; a spinner does not, so an app shows a Skeleton wherever content loads.
Use instead when
- Use
Button'sloadinginstead for an action in progress, andProgresswhen you know how far along the work is.
Props
The props the modes share. Type: SkeletonProps.
className- Type:
string - Layout only: size, margins and placement (
h-32,w-1/2). The color and corners come from the theme andshape. ref- Type:
Ref<HTMLSpanElement> - Reaches the rendered element.
Modes
SkeletonProps is a union: pass the props of exactly one mode. Mixing them is a type error.
Circle: SkeletonCircleProps
Skeleton's props for an avatar.
shape(required)- Type:
"circle" - The outline of what is loading:
rect(a block),text(lines),circle(an avatar),row(a list row) orcard. size- Type:
"sm" | "md" | "lg" | "xs" | "xl" | "2xl" | "3xl" - Default:
"md" - Avatar's size, so it is the avatar's exact box. A layout class (
size-12) sizes it instead.
Not in this mode (typed never):
count: Only withshape="row"or"card".lines: Only withshape="text".
Rect: SkeletonRectProps
Skeleton's props for a block.
shape- Type:
"rect" - Default:
"rect" - The outline of what is loading:
rect(a block),text(lines),circle(an avatar),row(a list row) orcard.
Not in this mode (typed never):
count: Only withshape="row"or"card".lines: Only withshape="text".size: Only withshape="text","circle","row"or"card". Size a block with a layout class.
Row or card: SkeletonRowOrCardProps
Skeleton's props for list rows or Cards.
count- Type:
number - Default:
1 - How many rows or cards to show. Each is its own element, so they sit in the grid or the
divthe content will fill (not directly in aul: the rows arespans withdisplay: grid). shape(required)- Type:
"row" | "card" - The outline of what is loading:
rect(a block),text(lines),circle(an avatar),row(a list row) orcard. size- Type:
"sm" | "md" - Default:
"md" - Item's or Card's size, so each placeholder is that component's exact height.
Not in this mode (typed never):
lines: Only withshape="text".
Text: SkeletonTextProps
Skeleton's props for lines of text.
lines- Type:
number - Default:
1 - How many lines of text it stands in for; the last of several is shorter, like a paragraph's.
shape(required)- Type:
"text" - The outline of what is loading:
rect(a block),text(lines),circle(an avatar),row(a list row) orcard. size- Type:
"sm" | "md" | "lg" - Text's size, so each line is one line box of that text. Leave it out to inherit the surrounding text's size.
Not in this mode (typed never):
count: Only withshape="row"or"card". For several lines uselines.
Native attributes
Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
animation: Not a prop. Skeleton shimmers, and holds still with reduced motion.height: Not a prop. Size it with a layout class, or giveshape="text"itslines:<Skeleton className="h-6">.variant: Not a prop. Ouro UI usesshape:<Skeleton shape="circle">.width: Not a prop. Size it with a layout class:<Skeleton className="w-32">.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
