# Skeleton

> Reserve space while content is still loading.

- Category: Cards
- Import: `import { Skeleton } from "@sagui/ui";`
- Page: /components/skeleton

```tsx
import { Skeleton } from "@sagui/ui";

export function Hero() {
  return (
    <div className="w-full max-w-sm">
      <Skeleton avatar lines={3} />
    </div>
  );
}
```

## When to use

- Loading states for content whose shape is known, like a profile or comment.
- Swapping a placeholder into real content with a crossfade and height spring.

## When not to use

- Use progress when you can report a percentage.
- Use empty-state when loading finished and there is nothing to show.
- Use text-shimmer for an AI thinking or status line.

## Installation

Install the package and import the styles once. The [installation guide](/docs/installation) covers the Tailwind setup.

```bash
npm install @sagui/ui
```

## Usage

```tsx
import { Skeleton } from "@sagui/ui";

export function Profile({ user }: { user?: User }) {
  return (
    <Skeleton avatar lines={2} loading={!user}>
      {user && <ProfileCard user={user} />}
    </Skeleton>
  );
}
```

## Examples

### Revealing content

Wrap the real content and set `loading`. The placeholder crossfades into it when loading ends.

```tsx
import { useState } from "react";
import { Button, Skeleton } from "@sagui/ui";

export function Reveal() {
  const [loading, setLoading] = useState(true);
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Skeleton loading={loading} lines={2}>
        <div>
          <p className="type-title">Acme renewal</p>
          <p className="type-body-sm text-muted-foreground">$42,000 · closes in 12 days</p>
        </div>
      </Skeleton>
      <Button size="sm" variant="outline" className="w-fit" onClick={() => setLoading((value) => !value)}>{loading ? "Finish loading" : "Load again"}</Button>
    </div>
  );
}
```

## API reference

### Skeleton

A pulsing placeholder of text lines and an optional avatar that can crossfade into the loaded content.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"Loading content"` | aria-label of the loading status. |
| `lines` | `number` | `3` | Number of text lines, clamped to 1-6. |
| `avatar` | `boolean` | `false` | Adds a round avatar placeholder. |
| `children` | `ReactNode` | – | Content to reveal once loading finishes. With children the placeholder crossfades into them. |
| `loading` | `boolean` | `true` | Keeps the placeholder visible while true. Only used with children. |
| `className` | `string` | – | Class on the outer element. |

## Accessibility

- The placeholder is role="status" with aria-busy and an aria-label; its shapes are aria-hidden.
- With children, the wrapper sets aria-busy while loading.

## Motion

- Blocks pulse in a staggered wave, each a beat after the one above.
- On load the placeholder fades out, content rises 4px into place, and the height springs from placeholder to content.
- Reduced motion stops the pulse and swaps with short fades and no height animation.

## Responsive behavior

- Lines are percentages of the container width, so the placeholder scales with its slot.
- It only draws text lines and an avatar; build custom shapes for grids or media.

## Performance

- The pulse is a CSS animation that stops under reduced motion.
- A ResizeObserver springs the height from placeholder to content; avoid hundreds of skeletons at once.

## Notes

- Use while fetching content whose shape is known. Use progress when you can report a percentage, and empty-state when there is nothing to show.
- Wrap the real content as children and drive loading to get the crossfade; without children it renders only the placeholder.

## Related

- [Empty state](/components/empty-state): A useful next step when there is nothing to show yet.
- [Card](/components/card): A contained group of related content and actions.
