Skip to content

Skeleton

Placeholder shapes that mirror the layout of content while it loads, so the page does not jump when the data arrives. Three shapes (text line, circle, rectangle); size anything with className.

Installation

Add Skeleton to your project with the CK-UI CLI. It copies the source into your project, so the code is yours to change.

Terminal
npx ck-ui-cli@latest add skeleton
Adds
Skeleton.tsxto your CK-UI folder (src/components/ck-ui by default)
Installs
No extra npm packages

First time with CK-UI in this project? Run npx ck-ui-cli@latest init once before adding components. The installation guide explains what it sets up.

Manual installationInstall the packages and copy the files yourself.

Create or update your Skeleton component

Copy this code to create your Skeleton component

/components/ui/Skeleton.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

/*
 * Placeholder shapes shown while content loads. Skeletons are decorative (aria-hidden): announce the
 * loading state on the container instead, e.g. aria-busy="true" plus visually hidden "Loading…" text.
 * Server-compatible. The pulse stops under prefers-reduced-motion.
 */

export type SkeletonShape = "text" | "circle" | "rect";

const shapeClasses: Record<SkeletonShape, string> = {
    text: "h-4 w-full rounded-md",
    circle: "size-10 shrink-0 rounded-full",
    rect: "h-32 w-full rounded-2xl",
};

// foreground/10 stays visible on background, surface and muted in both themes.
const base = "block animate-pulse bg-foreground/10 motion-reduce:animate-none";

export interface SkeletonRootProps extends React.HTMLAttributes<HTMLDivElement> {
    /** Default size and rounding. Size anything with `className` (e.g. `h-6 w-24`). */
    shape?: SkeletonShape;
}

const Root = React.forwardRef<HTMLDivElement, SkeletonRootProps>(({ shape = "text", className, ...props }, ref) => (
    <div ref={ref} aria-hidden="true" data-shape={shape} className={cn(base, shapeClasses[shape], className)} {...props} />
));
Root.displayName = "Skeleton.Root";

export interface SkeletonTextProps extends React.HTMLAttributes<HTMLDivElement> {
    /** Number of lines. The last one is shorter, like the end of a paragraph. */
    lines?: number;
    /** Classes for every line. */
    lineClassName?: string;
}

const Text = React.forwardRef<HTMLDivElement, SkeletonTextProps>(({ lines = 3, lineClassName, className, ...props }, ref) => {
    const count = Math.max(1, Math.floor(lines));

    return (
        <div ref={ref} aria-hidden="true" className={cn("flex w-full flex-col gap-2", className)} {...props}>
            {Array.from({ length: count }, (_, index) => (
                <div key={index} data-shape="text" className={cn(base, shapeClasses.text, count > 1 && index === count - 1 && "w-4/5", lineClassName)} />
            ))}
        </div>
    );
});
Text.displayName = "Skeleton.Text";

export { Root, Text };

Test Your Component

  • 1. Add the component to a test page
  • 2. Check all props and variants work as expected
  • 3. Verify responsive behavior
  • 4. Test with different content/data
  • 5. Ensure accessibility features work correctly

Profile

A circle for the avatar and two text lines.

Loading profile…

Card

A rectangle for media, then a heading line and Skeleton.Text for the body.

Loading article…

List

Repeat a row skeleton for tables and lists.

Loading invoices…

Paragraph

Skeleton.Text renders several lines with a shorter last line. Toggle to swap it for the real content.

Loading summary…

Accessibility

  • Skeletons are decorative and carry aria-hidden="true"; screen readers skip them.
  • Announce the loading state on the container instead: aria-busy="true" while loading plus a visually hidden "Loading…" text, as in the examples.
  • The pulse animation stops when the user prefers reduced motion.
  • The fill is foreground/10, so the shapes stay visible on background, surface and muted areas in light and dark mode.

API Reference

Server-compatible. Import with import * as Skeleton from "@/components/ui/Skeleton".

Skeleton.Root

PropTypeDefaultDescription
shape
"text""circle""rect"
"text"

Default size and rounding: text h-4 full width, circle 40px, rect h-32 full width.

className"string""undefined"

Size or round it differently, e.g. h-6 w-24. Merged last.

Skeleton.Text

Several text lines; the last one is shorter.

PropTypeDefaultDescription
lines"number""3"

Number of lines.

lineClassName"string""undefined"

Classes for every line.

className"string""undefined"

Classes for the wrapper.