Skip to main content

Display

Skeleton

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then 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.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/skeleton

Its 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.10
import { 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's loading instead for an action in progress, and Progress when 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 and shape.
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) or card.
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 with shape="row" or "card".
  • lines: Only with shape="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) or card.

Not in this mode (typed never):

  • count: Only with shape="row" or "card".
  • lines: Only with shape="text".
  • size: Only with shape="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 div the content will fill (not directly in a ul: the rows are spans with display: grid).
shape (required)
Type: "row" | "card"
The outline of what is loading: rect (a block), text (lines), circle (an avatar), row (a list row) or card.
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 with shape="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) or card.
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 with shape="row" or "card". For several lines use lines.

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 give shape="text" its lines: <Skeleton className="h-6">.
  • variant: Not a prop. Ouro UI uses shape: <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.