# Skeleton

A React skeleton loader for Next.js and Tailwind CSS: placeholder blocks while content loads, with a calm sheen that sweeps every skeleton in step.

Source: https://ui.fewtone.com/components/skeleton

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/skeleton.json
```

## Usage

```tsx
import { Skeleton } from "@/components/ui/skeleton"

export function ProfileLoading() {
  return (
    <div aria-busy="true" className="flex items-center gap-3">
      <span className="sr-only">Loading profile</span>
      <Skeleton className="size-10 rounded-full" />
      <div className="grid gap-2">
        <Skeleton className="h-4 w-40" />
        <Skeleton className="h-4 w-24" />
      </div>
    </div>
  )
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | – | Its size and shape. Rounded md by default; rounded-full for a circle. |

**When to use it:** Use Skeleton for placeholders shaped like the content that's loading (a profile row, a card, a table), so the page doesn't jump when it arrives. Mark the loading region aria-busy="true" with a visually hidden label. Use Spinner for a small busy state with no layout to hold, such as a button.
**npm packages (installed by the CLI):** motion@^14.0.0.

## Examples

### Placeholders

A profile row and a card. A sheen one step lighter sweeps across, every skeleton in step; with reduced motion they're still.

```tsx
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";

/*
 * A profile row and a card, each named underneath. Every skeleton sweeps in step. The skeletons are hidden from
 * screen readers; each loading region says so with aria-busy and a visually hidden label.
 */
export default function SkeletonPlaceholders() {
  return (
    <ul className="flex w-full flex-wrap items-end justify-center gap-x-12 gap-y-8">
      <li className="flex flex-col items-center gap-3">
        <div aria-busy="true" className="flex w-64 items-center gap-3">
          <span className="sr-only">Loading profile</span>
          <Skeleton className="size-10 shrink-0 rounded-full" />
          <div className="grid flex-1 gap-2">
            <Skeleton className="h-4 w-3/4" />
            <Skeleton className="h-4 w-1/2" />
          </div>
        </div>
        <span className="text-xs text-muted-foreground">Profile</span>
      </li>
      <li className="flex flex-col items-center gap-3">
        <Card aria-busy="true" className="w-64">
          <span className="sr-only">Loading post</span>
          <CardHeader>
            <Skeleton className="h-4 w-2/3" />
            <Skeleton className="h-4 w-1/2" />
          </CardHeader>
          <CardContent>
            <Skeleton className="aspect-video w-full" />
          </CardContent>
        </Card>
        <span className="text-xs text-muted-foreground">Card</span>
      </li>
    </ul>
  );
}
```

### Loading, then content

Reload: the skeleton fades in while the region is aria-busy, then the content comes back through the blur in the same place.

```tsx
"use client";

import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { blur, duration, ease } from "@/lib/motion";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";

/*
 * Reload: the profile gives way to its skeleton, and comes back through the blur when it has "loaded". Both sit
 * in one grid cell, so nothing below moves. The skeleton only fades (it's a fill), the content sharpens in.
 * While it loads, the region is aria-busy and a status says so.
 */
export default function SkeletonLoading() {
  const [loading, setLoading] = useState(false);
  const reduce = useReducedMotion() ?? false;

  // Stands in for a request
  useEffect(() => {
    if (!loading) return;
    const timer = setTimeout(() => setLoading(false), 1500);
    return () => clearTimeout(timer);
  }, [loading]);

  return (
    <div className="grid w-full max-w-xs justify-items-start gap-6">
      <div aria-busy={loading} className="grid w-full">
        <AnimatePresence initial={false}>
          {loading ? (
            <motion.div
              key="skeleton"
              className="col-start-1 row-start-1 flex items-center gap-3"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: duration.fast, ease: ease.out }}
            >
              <Skeleton className="size-10 shrink-0 rounded-full" />
              <div className="grid flex-1 gap-2">
                <Skeleton className="h-4 w-3/4" />
                <Skeleton className="h-4 w-1/2" />
              </div>
            </motion.div>
          ) : (
            <motion.div
              key="profile"
              className="col-start-1 row-start-1 flex items-center gap-3"
              initial={{ opacity: 0, filter: reduce ? blur.shown : blur.hidden }}
              animate={{ opacity: 1, filter: blur.shown }}
              exit={{ opacity: 0, transition: { duration: duration.fast, ease: ease.in } }}
              transition={{ duration: duration.base, ease: ease.out }}
            >
              <Avatar size="lg">
                <AvatarFallback>SD</AvatarFallback>
              </Avatar>
              <div className="grid min-w-0">
                <span className="truncate text-sm font-medium">Sofia Davis</span>
                <span className="truncate text-sm text-muted-foreground">Design lead, Lisbon</span>
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
      <span role="status" className="sr-only">
        {loading ? "Loading profile" : ""}
      </span>
      <Button variant="outline" size="sm" onClick={() => setLoading(true)}>
        Reload
      </Button>
    </div>
  );
}
```

## Related

- [Spinner](https://ui.fewtone.com/components/spinner.md): An accessible React spinner for Next.js and Tailwind CSS: one line going round an infinity that fills it once the task is done.
- [Card](https://ui.fewtone.com/components/card.md): A React card for Next.js and Tailwind CSS: a surface for grouped content with header, action, content and footer parts, a small size and a link style.
- [Avatar](https://ui.fewtone.com/components/avatar.md): An accessible React avatar for Next.js and Tailwind CSS: a picture or initials in three sizes, alone or in an overlapping group, built on Radix Avatar.
