<!-- Protocore Design System — LoadingSkeleton -->
# LoadingSkeleton

- **Category:** Feedback (`feedback`)
- **Slug:** `feedback/loading-skeleton`
- **Status:** stable
- **Platforms:** web, mobile
- **Import:** `import { LoadingSkeleton, SkeletonText, SkeletonBlock } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/feedback/loading-skeleton

> Shimmering placeholder blocks that hold a layout while its real content loads.

## The five-state doctrine

Every data-driven surface in Protocore renders one of **five states** — never a blank frame. Design all five up front:

1. **Loading** — the shape of the content, not a spinner in the void: a `LoadingSkeleton` that holds the layout.
2. **Empty** — the request succeeded but there is legitimately nothing: [EmptyState](/feedback/empty-state).
3. **Error** — the request failed: [ErrorState](/feedback/error-state), with a retry and a `debugId`.
4. **Pending** — a mutation is in flight over already-rendered data: a [Spinner](/feedback/spinner) or [ProgressBar](/feedback/progress-bar) layered on top, content still visible.
5. **Data** — the happy path.

This is the *console pattern*: a panel is a small state machine, and a skeleton is what the machine shows on entry. Skipping straight from nothing to data is the drift this doctrine exists to prevent.

## When to use it

Use a skeleton for the **first load** of a known layout — a table, a card grid, a detail panel. Prefer it over a bare [Spinner](/feedback/spinner) whenever you can predict the shape of the result, because it reduces layout shift and perceived latency.

For a mutation over content that is *already* on screen, don't swap it for a skeleton — keep the data and layer a pending affordance instead.

## Mobile (React Native)

**Preview.** `@protocore/pds-mobile` ships the React Native sibling of **LoadingSkeleton**. It mirrors the web API where React Native allows; the package is a **preview** with no device-level QA yet, so pin it and expect small changes.

Import it from the mobile package (not `@protocore/pds`), inside a `<PdsProvider>` — there is no stylesheet, so `style` (a `ViewStyle`) replaces `className` and every value comes from the theme:

```tsx
import { LoadingSkeleton, SkeletonText, SkeletonBlock } from "@protocore/pds-mobile";
```

**Parity with web.** Shimmering placeholder blocks, plus the `SkeletonText` / `SkeletonBlock` presets.

- Web animates a moving linear-gradient; RN has no CSS gradients, so the shimmer is an `Animated` opacity pulse over a `fill-ghost` block.
- `lines`, `height`, and `width` match the web API.

```tsx
<LoadingSkeleton lines={3} />
```

## Props

### LoadingSkeleton

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `height` | `string \| number` | no | `96` | Height of each bar (number = px). Defaults to a text line. |
| `lines` | `number` | no | `3` | How many stacked placeholder bars to render. |
| `style` | `CSSProperties` | no | — | — |
| `width` | `string \| number` | no | — | Width of each bar (number = px). Defaults to full width. |

### SkeletonText

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `lines` | `number` | no | `3` | Number of text lines to fake. |
| `style` | `CSSProperties` | no | — | — |
| `width` | `string \| number` | no | — | Width of the final (short) line. |

### SkeletonBlock

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `height` | `string \| number` | no | `96` | Block height (number = px). |
| `style` | `CSSProperties` | no | — | — |
| `width` | `string \| number` | no | — | Block width (number = px). |

## Examples

### Basics

`LoadingSkeleton` renders a stack of shimmering bars. The last bar is shortened automatically so the group reads as a paragraph rather than a block.

```tsx
import { LoadingSkeleton } from "@protocore/pds";

export default function Basics() {
  return <LoadingSkeleton lines={3} />;
}
```

### Text and block presets

`SkeletonBlock` is a single solid shape for avatars, thumbnails and media; `SkeletonText` fakes lines of copy. Compose them to mirror the real layout.

```tsx
import { HStack, SkeletonBlock, SkeletonText, VStack } from "@protocore/pds";

export default function Presets() {
  return (
    <HStack gap={4} align="start">
      <SkeletonBlock height={48} width={48} />
      <VStack gap={2} style={{ flex: 1 }}>
        <SkeletonText lines={2} width="40%" />
      </VStack>
    </HStack>
  );
}
```

## Do & don't

**Do**

- Match the skeleton's size and count to the real content it stands in for.
- Use it for first loads of predictable, structured layouts.
- Compose SkeletonBlock + SkeletonText to mirror the true shape.
- Swap to the real content — or an EmptyState / ErrorState — as soon as the request resolves.

**Don't**

- Show a skeleton whose shape doesn't match what replaces it.
- Keep skeletons up indefinitely — cap the wait, then fall through to an ErrorState.
- Skeleton content that is already rendered during a background refresh.
- Add your own `aria-live` — the skeleton is deliberately `aria-hidden`.

## Accessibility

**Notes**

- Skeletons are decorative: the root carries `aria-hidden="true"`, so nothing is announced.
- Announce the wait elsewhere — a `role="status"` region, a Spinner's label, or an aria-busy container around the region.
- The shimmer respects `prefers-reduced-motion`.
- Because it is hidden from assistive tech, never convey information (like a count) through skeleton bars alone.

## Related

`spinner`, `empty-state`, `error-state`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
