Data display

Skeleton

Shimmer placeholder while content loads.

Preview
Install
npx shadcn@latest add https://ui.thjsecurity.tech/r/skeleton.json

Pulls the component into components/ui/skeleton.tsx and its token dependencies. The app owns the copy from then on.

Source
import { cn } from "@/lib/utils"

/** A placeholder block with the shimmer sweep (`.animate-shimmer` in globals.css). */
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn("animate-shimmer rounded-lg bg-glass-strong", className)} {...props} />
}

/** `rows` list rows of the given height, widths varied so the block reads as text, not bars. */
function SkeletonRows({ rows = 6, height = "h-10", className }: { rows?: number; height?: string; className?: string }) {
  const widths = ["w-full", "w-[92%]", "w-[97%]", "w-[88%]", "w-full", "w-[94%]"]
  return (
    <div className={cn("space-y-2", className)} aria-busy="true">
      {Array.from({ length: rows }, (_, i) => (
        <Skeleton key={i} className={cn(height, widths[i % widths.length])} />
      ))}
    </div>
  )
}

/** A stat strip's worth of placeholders: a number and a label per cell. */
function SkeletonStats({ cells = 4, className }: { cells?: number; className?: string }) {
  return (
    <div className={cn("grid grid-cols-2 gap-6", cells >= 5 ? "md:grid-cols-5" : cells === 3 ? "md:grid-cols-3" : "md:grid-cols-4", className)} aria-busy="true">
      {Array.from({ length: cells }, (_, i) => (
        <div key={i} className="space-y-2">
          <Skeleton className="h-7 w-16" />
          <Skeleton className="h-3 w-24" />
        </div>
      ))}
    </div>
  )
}

export { Skeleton, SkeletonRows, SkeletonStats }