# Skeleton

> Placeholder lines, avatars and blocks shown while content loads, driven by aria-busy.

Page: https://pico.rusticstack.com/docs/extras/skeleton

A container with `aria-busy="true"` shows its empty `data-skeleton` children as placeholders. The values are `line`, `avatar` and `block`. Set the width of a line with `inline-size`. When the data arrives, set `aria-busy="false"` or swap the content.

Load `css/extras/skeleton.min.css`, or the whole set from `css/pico.extras.min.css`. See the [overview](/docs/extras/overview).

A container that holds skeletons does not show the core `aria-busy` spinner. A container with `aria-busy="true"` and no skeletons keeps it (for example `<p aria-busy="true">Saving your changes</p>`).

## Card

Give the skeleton the shape of the content it stands in for: one line per line of text, an avatar only where the loaded card has an image. When the two match, the swap does not move the page.

**Example: Loading card**

```html
<article aria-busy="true" aria-label="Loading invoice">
  <header>
    <p data-skeleton="line" style="inline-size: 40%"></p>
  </header>
  <p data-skeleton="line"></p>
  <footer>
    <p data-skeleton="line" style="inline-size: 30%"></p>
  </footer>
</article>
```

The same card once the data is in, with `aria-busy="false"`:

```html
<article aria-busy="false">
  <header>
    <div style="display: flex; align-items: center; gap: var(--pico-spacing)">
      <strong>Acme Cloud</strong>
      <small>Invoice 2041, paid</small>
    </div>
  </header>
  <p>Your team plan renewed on 1 October. The next invoice is due on 1 November.</p>
  <footer><small>Billing contact: billing@acme.example</small></footer>
</article>
```

## Text lines

Use a `role="status"` container with an `aria-label` so screen readers announce the loading state and the finished content.

**Example: Loading text**

```html
<section role="status" aria-busy="true" aria-label="Loading comments">
  <p data-skeleton="line"></p>
  <p data-skeleton="line" style="inline-size: 90%"></p>
  <p data-skeleton="line" style="inline-size: 70%"></p>
</section>
```

## Avatar and lines

**Example: Loading profile**

```html
<div
  role="status"
  aria-busy="true"
  aria-label="Loading profile"
  style="display: flex; align-items: center; gap: var(--pico-spacing)"
>
  <span data-skeleton="avatar"></span>
  <div style="flex: 1">
    <p data-skeleton="line" style="inline-size: 50%"></p>
    <p data-skeleton="line" style="inline-size: 30%"></p>
  </div>
</div>
```

## Results swapped by htmx

A results list swapped by htmx follows the same pattern. The list starts busy with three lines, and htmx replaces the inner HTML when the response arrives. The new content has no skeletons, so set `aria-busy="false"` on the response, or let your code clear the attribute after the swap.

```html
<section
  id="results"
  aria-busy="true"
  aria-label="Search results"
  hx-get="/customers"
  hx-trigger="keyup changed delay:300ms from:input[name=q]"
  hx-target="this"
  hx-swap="innerHTML"
>
  <p data-skeleton="line"></p>
  <p data-skeleton="line" style="inline-size: 75%"></p>
  <p data-skeleton="line" style="inline-size: 55%"></p>
</section>
```

## Accessibility

- `aria-busy="true"` tells assistive technology the region is still updating. Name the region with `aria-label` so it is clear what is loading.
- The placeholders are empty and have no text, so there is nothing for a screen reader to read.
- The shimmer only runs under `prefers-reduced-motion: no-preference`. Everyone else gets the flat color.

## Customization

| Variable | Default | Description |
| --- | --- | --- |
| `--pico-skeleton-color` | `var(--pico-muted-border-color)` | Base color of a placeholder. |
| `--pico-skeleton-highlight-color` |  | Color of the moving highlight. |
| `--pico-skeleton-duration` | `1.5s` | Time for one sweep of the shimmer. |
| `--pico-skeleton-line-height` | `1em` | Height of a line. |
| `--pico-skeleton-avatar-size` | `3rem` | Diameter of an avatar placeholder. |
| `--pico-skeleton-block-size` | `8rem` | Height of a block. |

