# Skeleton

> Skeleton primitive with pulse or shimmer effect, plus SkeletonCard and SkeletonList compositions.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/skeleton
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/skeleton.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/skeleton-demo.tsx"
"use client"

import * as React from "react"

import { Skeleton, SkeletonCard, SkeletonList } from "@/components/skeleton"

export default function SkeletonDemo() {
  const [loaded, setLoaded] = React.useState(false)

  return (
    <div className="flex flex-col items-center gap-6">
      <button
        type="button"
        onClick={() => setLoaded((v) => !v)}
        className="inline-flex h-8 items-center rounded-lg border bg-background px-3 text-xs font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
      >
        {loaded ? "Show skeleton" : "Show content"}
      </button>
      <div className="flex flex-wrap items-start justify-center gap-6">
        {loaded ? (
          <div className="w-72 rounded-2xl border bg-card p-4 shadow-sm">
            <div className="flex h-36 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500/30 to-sky-500/30 text-sm font-medium">
              Loaded content
            </div>
            <p className="mt-4 text-sm font-medium">Ada Lovelace</p>
            <p className="mt-1 text-sm text-muted-foreground">Content replaces the skeleton once data arrives.</p>
          </div>
        ) : (
          <SkeletonCard className="w-72" />
        )}
        <SkeletonCard shimmer media={false} className="w-72" />
        <div className="flex w-72 flex-col gap-6">
          <SkeletonList rows={3} />
          <SkeletonList shimmer rows={2} />
          <div aria-busy="true" className="flex items-center gap-3">
            <Skeleton shimmer className="h-9 w-24 rounded-lg" />
            <Skeleton className="h-9 w-24 rounded-lg" />
          </div>
        </div>
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/skeleton.tsx"
import * as React from "react"

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

const css = `
@keyframes atelier-sk-shimmer{100%{transform:translateX(100%)}}
.atelier-sk-shimmer{position:relative;overflow:hidden}
.atelier-sk-shimmer::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background-image:linear-gradient(90deg,transparent,color-mix(in oklab,var(--foreground) 9%,transparent),transparent);animation:atelier-sk-shimmer 1.6s infinite}
@media (prefers-reduced-motion:reduce){.atelier-sk-shimmer::after{animation-duration:4.8s}}
`

export interface SkeletonProps extends React.ComponentProps<"div"> {
  /** Use a moving shimmer highlight instead of a pulse. */
  shimmer?: boolean
}

export function Skeleton({ shimmer = false, className, ...props }: SkeletonProps) {
  return (
    <>
      {shimmer && (
        <style href="atelier-skeleton" precedence="default">
          {css}
        </style>
      )}
      <div
        aria-hidden="true"
        className={cn(
          "rounded-md bg-muted",
          shimmer ? "atelier-sk-shimmer" : "animate-pulse motion-reduce:[animation-duration:4s]",
          className
        )}
        {...props}
      />
    </>
  )
}

export interface SkeletonCardProps extends React.ComponentProps<"div"> {
  shimmer?: boolean
  /** Show a media block above the text. */
  media?: boolean
}

export function SkeletonCard({ shimmer = false, media = true, className, ...props }: SkeletonCardProps) {
  return (
    <div
      aria-busy="true"
      className={cn("w-full max-w-xs overflow-hidden rounded-2xl border bg-card p-4 shadow-sm", className)}
      {...props}
    >
      <span className="sr-only">Loading</span>
      {media && <Skeleton shimmer={shimmer} className="h-36 w-full rounded-xl" />}
      <div className={cn("flex items-center gap-3", media && "mt-4")}>
        <Skeleton shimmer={shimmer} className="size-10 shrink-0 rounded-full" />
        <div className="flex-1 space-y-2">
          <Skeleton shimmer={shimmer} className="h-3.5 w-2/3" />
          <Skeleton shimmer={shimmer} className="h-3 w-1/3" />
        </div>
      </div>
      <div className="mt-4 space-y-2">
        <Skeleton shimmer={shimmer} className="h-3 w-full" />
        <Skeleton shimmer={shimmer} className="h-3 w-5/6" />
      </div>
    </div>
  )
}

export interface SkeletonListProps extends React.ComponentProps<"div"> {
  shimmer?: boolean
  rows?: number
}

export function SkeletonList({ shimmer = false, rows = 4, className, ...props }: SkeletonListProps) {
  return (
    <div aria-busy="true" className={cn("w-full max-w-sm space-y-4", className)} {...props}>
      <span className="sr-only">Loading</span>
      {Array.from({ length: rows }, (_, i) => (
        <div key={i} className="flex items-center gap-3">
          <Skeleton shimmer={shimmer} className="size-10 shrink-0 rounded-lg" />
          <div className="flex-1 space-y-2">
            <Skeleton shimmer={shimmer} className={cn("h-3.5", i % 2 ? "w-1/2" : "w-3/4")} />
            <Skeleton shimmer={shimmer} className="h-3 w-1/3" />
          </div>
        </div>
      ))}
    </div>
  )
}
```
