# Skeleton

> A pulsing placeholder shape that holds the place of content while it loads. Hidden from screen readers.

- Name: `skeleton`
- Status: beta
- Categories: feedback
- File: the install writes `skeleton.tsx` to your `ui` alias, `@/components/ui` by default.
- Gallery: https://arbokui.com/components/skeleton
- Registry item: https://arbokui.com/r/skeleton.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/skeleton
   ```

## Use it for

- Holding the place of content that is loading, at the same size and shape, so the page does not jump when it arrives.
- Size and shape it with className: h-4 w-48 for a line of text, size-10 rounded-full for an avatar.
- Put aria-busy="true" on the region that is loading. Say it in words in a role="status" element that stays on the page, outside that region.

## Avoid it for

- Wrapping real content. A skeleton takes no children. Render it in place of the content, not around it.
- An action in progress, like saving. Use loading on the Button.
- Loads that finish in well under a second. The skeleton only flashes. Show nothing until the content arrives.
- Layouts you cannot predict. If the shape will not match, a short status message is clearer.

## Props

- `className`: `string`. Size and shape. Later classes win, so className='rounded-full' replaces the radius and 'animate-none' stops the pulse.
- `aria-hidden`: `boolean`. Default `true`. Set for you, so screen readers skip the shape.
- `children`: `never`. Not allowed. TypeScript rejects content inside a skeleton, because aria-hidden would hide it.

## Accessibility

- Renders a \<span> with aria-hidden, so screen readers skip it. Its shape only means something to people who can see it.
- Tell screen readers in words. Keep one role="status" element on the page and change its text: "Loading orders" while the skeletons show, then "Orders loaded".
- Put that status element outside the loading region. Screen readers may wait to read changes inside a region marked aria-busy, and some miss a status region that arrives together with its text.
- Add aria-busy="true" to the region that is loading, and remove it when the content arrives.
- It is a \<span> with display block, so it is valid inside a paragraph, a heading, or a button.
- It pulses at --motion-pulse-duration. The theme sets that to 0s for motion off and for reduced motion, so the shape stays still then.
- The fill is the theme's --color-skeleton, the foreground at 10%, so the shape shows on the page, on cards, and on muted panels. A placeholder is not text, so the theme does not hold it to a contrast minimum.

## Examples

### A line of text

```tsx
<Skeleton className="h-4 w-48" />
```

### A profile row, with a status that stays on the page

```tsx
function Profile({ user }: { user: { name: string; role: string; avatar: string } | null }) {
  return (
    <>
      <div aria-busy={user === null} className="flex items-center gap-3">
        {user === null ? (
          <>
            <Skeleton className="size-10 rounded-full" />
            <div className="flex flex-col gap-2">
              <Skeleton className="h-4 w-32" />
              <Skeleton className="h-3 w-24" />
            </div>
          </>
        ) : (
          <>
            <img src={user.avatar} alt="" className="size-10 rounded-full" />
            <div>
              <p className="font-medium">{user.name}</p>
              <p className="text-sm text-muted-foreground">{user.role}</p>
            </div>
          </>
        )}
      </div>
      <p role="status" className="sr-only">
        {user === null ? "Loading profile" : "Profile loaded"}
      </p>
    </>
  );
}
```

### A card while it loads (needs @arbokui/card)

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

// Pair it with a role="status" element outside the card, as in the profile row.
<Card aria-busy="true">
  <CardHeader>
    <Skeleton className="h-5 w-1/2" />
    <Skeleton className="h-4 w-3/4" />
  </CardHeader>
  <CardContent>
    <Skeleton className="h-24 w-full" />
  </CardContent>
</Card>
```
