← ui

skeleton

ui

the shape of something still coming, breathing slowly while it loads

npx shadcn add @ag/skeleton
Already installed? Preview an update
npx shadcn add @ag/skeleton --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-10
  • New: the shape of something still coming, breathing slowly while it loads, still with reduced motion, hidden from a screen reader; plain or a pane of glass.
Updated 2026-10-10 · release notes

also called placeholder, loading shimmer

shelf controls

uses material

used by components

the demo above, verbatim

skeleton.demo.tsx
"use client"

import { Sample } from "@/app/samples"
import { MaterialProvider } from "@/registry/base-nova/ui/material"
import { Skeleton } from "@/registry/base-nova/ui/skeleton"

// #region post
function Post() {
  return (
    <div aria-busy className="flex w-64 items-center gap-3">
      {/* A glass skeleton states its radius as a length (half the height, for a
          round one): it places the glass's light as well as its corners. */}
      <Skeleton className="size-10 shrink-0 rounded-full [--glass-radius:1.25rem]" />
      <div className="flex flex-1 flex-col gap-2">
        <Skeleton className="h-3 w-3/4 [--glass-radius:6px]" />
        <Skeleton className="h-3 w-1/2 [--glass-radius:6px]" />
      </div>
    </div>
  )
}
// #endregion

export default function SkeletonDemo() {
  return (
    <div className="space-y-8">
      <Sample name="materials" title="a post still coming" with="post">
        <div className="grid gap-4 sm:grid-cols-2">
          <Post />
          <MaterialProvider material="glass">
            <div className="rounded-xl bg-[url(/lightbox/shore.jpg)] bg-cover bg-center p-6">
              <Post />
            </div>
          </MaterialProvider>
        </div>
      </Sample>
      <Sample name="post" glance title="one post, still coming" with="post">
        <Post />
      </Sample>
    </div>
  )
}

the source, every file an install writes

skeleton.tsx
"use client"

import type * as React from "react"
import { cn } from "@/lib/utils"
import {
  type Material,
  type Scheme,
  useMaterial,
  useSurfaceScheme,
} from "@/registry/base-nova/ui/material"
import { glassVariants, schemeOf } from "@/registry/base-nova/ui/material-glass"

// The shape of something still coming: a block where it will be, breathing slowly
// while it loads and gone once it has (it is shown only while the work runs, so it
// never breathes for nothing). Size it as what it stands for (`h-4 w-40` a line,
// `size-10 rounded-full [--glass-radius:1.25rem]` a face: the glass states its
// radius as a length, which places its light too). Hidden from a screen reader: the
// region that waits says so (`aria-busy`). With reduced motion it holds still.
// `material="glass"`: a pane of the glass the content will sit on.

export type SkeletonProps = React.ComponentProps<"div"> & {
  material?: Material
  /** The colour scheme it stands in (material): default the nearest provider's. */
  scheme?: Scheme
}

export function Skeleton({
  material,
  scheme,
  className,
  ref,
  ...props
}: SkeletonProps) {
  const m = useMaterial(material)
  const [s, here] = useSurfaceScheme<HTMLDivElement>(scheme, ref)
  return (
    <div
      ref={here}
      aria-hidden
      data-slot="skeleton"
      data-material={m}
      className={cn(
        "animate-pulse [animation-duration:2s] motion-reduce:animate-none",
        schemeOf(s),
        m === "glass"
          ? [
              glassVariants({ shape: "surface" }),
              "[--glass-radius:--theme(--radius-xl)]",
            ]
          : "rounded-md bg-foreground/8",
        className,
      )}
      {...props}
    />
  )
}