Skeleton
Shimmer placeholder while content loads.
npx shadcn@latest add https://ui.thjsecurity.tech/r/skeleton.jsonPulls the component into components/ui/skeleton.tsx and its token dependencies. The app owns the copy from then on.
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 }