Skip to contentVibraUI
Feedback & status

Skeleton

A muted placeholder block that stands in for content while it loads.

Vibra's skeleton pulses only for readers who have not asked for less motion — by the OS setting or a [data-motion="reduced"] above it — where shadcn's always does, and a shade quicker, at 1.2s rather than 2s; SkeletonReveal fades the loaded content in over --duration-slow, so a page settling reads as one movement. Keep what you already know — a column title, a card title, a field name — and draw only what is on its way, in its shape. The skeletons are decoration: hide them from screen readers, mark the region that is filling aria-busy, and say what is loading in words, once for the region. LoadingSkeletons has ready shapes for a stat, a table, a chart, a list and a form.

Install

npx shadcn@latest add @vibra/skeleton

Needs the @vibra registry in your components.json — set it up once.

Examples

A row of figures

The card titles stay; the numeral and its delta wait in their own shapes, and the row is one busy region.

Table rows

The column titles are the table's own and only the body waits, the rows hidden from screen readers.

Paragraphs

A release note's body as lines of prose, each paragraph ending short, under the heading that is already known.

A record page

A customer's face, name and values in their shapes beside the field names, which are the page's own.

Then the content

SkeletonReveal fades the rows in where the skeleton stood; the list is busy only while it waits.

Held still when asked

An app's own Reduce motion switch sets data-motion="reduced", which stops the pulse as the OS setting does.

Props

PropTypeDefaultDescription
SkeletonRevealReact.ComponentProps<"div">—Wraps what replaces a skeleton and fades it in; under reduced motion it simply appears.

Dependencies

Registry

Source

components/ui/skeleton.tsx
import { cn } from "@/lib/utils"

/**
 * The placeholder plane: the warm recessed track, breathing at 1.2s.
 *
 * `motion-safe:` rather than an unconditional animation, so a reader who has
 * asked the system for less motion gets a still block instead of a page of
 * pulsing rectangles — and so the reduced-motion census finds nothing running
 * after load. The theme's other switch, a `[data-motion="reduced"]` a page or
 * a preview frame sets above its content, stops it too: a loop takes no
 * duration token, so it has to not run rather than run in zero time. The pace
 * is a shade quicker than Tailwind's 2s default: at two seconds a table of
 * forty bars reads as a page that has stalled.
 */
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="skeleton"
      className={cn(
        "rounded-md bg-muted motion-safe:not-in-data-[motion=reduced]:animate-pulse motion-safe:[animation-duration:1.2s]",
        className
      )}
      {...props}
    />
  )
}

/**
 * What replaces a skeleton, fading in over `--duration-slow` rather than
 * appearing outright — the same beat the sheets enter on, so a page settling
 * reads as one movement instead of a series of pops.
 */
function SkeletonReveal({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="skeleton-reveal"
      className={cn(
        "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-(--duration-slow) motion-safe:ease-(--ease-standard)",
        className
      )}
      {...props}
    />
  )
}

export { Skeleton, SkeletonReveal }