# Skeleton (/docs/skeleton)





`Skeleton` is a loading placeholder. Standalone, it's a fixed rectangular or circular shape; wrapped around real content, it sizes itself to that content's own box and reveals it once loading finishes. [`DataTable`](/docs/data-table)'s own skeleton rows and cards render through this component internally.

```tsx
import { Skeleton } from '@42-central/ui-react/skeleton';

<Skeleton />
<Skeleton shape="circular" width={40} height={40} />

<Skeleton loading={isLoading}>
  <UserAvatar user={user} />
</Skeleton>
```

## Playground [#playground]

<story.Playground />

### Full prop browser [#full-prop-browser]

<story.WithControl />

## Shapes [#shapes]

Two shapes: `rectangular` (the default — fills its container's width) and `circular` (a fixed-diameter dot).

<Preview align="block" code="&#x22;<Skeleton shape=\&#x22;rectangular\&#x22; />\n<Skeleton shape=\&#x22;circular\&#x22; />&#x22;">
  <div className="flex w-full items-center gap-3">
    <Skeleton shape="rectangular" className="max-w-64" />

    <Skeleton shape="circular" />
  </div>
</Preview>

## Sizes [#sizes]

`size` steps through the kit's shared `xs`–`xl` scale — the same steps `DataTable`'s own density prop uses, so a skeleton bar matches whatever table density it stands in for. It's tuned for text-line bars (8–24px tall); for a bigger shape like an avatar circle, use `width`/`height` instead (see [Recipes](#recipes)).

<Preview align="block" code="&#x22;<Skeleton size=\&#x22;xs\&#x22; />\n<Skeleton size=\&#x22;sm\&#x22; />\n<Skeleton size=\&#x22;md\&#x22; />\n<Skeleton size=\&#x22;lg\&#x22; />\n<Skeleton size=\&#x22;xl\&#x22; />&#x22;">
  <div className="flex w-full max-w-64 flex-col gap-2">
    <Skeleton size="xs" />

    <Skeleton size="sm" />

    <Skeleton size="md" />

    <Skeleton size="lg" />

    <Skeleton size="xl" />
  </div>
</Preview>

## Wrap mode [#wrap-mode]

Pass `children` and Skeleton sizes a placeholder to match — no measurement, no `ResizeObserver`: the real content stays mounted (just made invisible) while loading, so it keeps contributing its own box to the wrapper the placeholder fills. Click below to see the same `Skeleton` instance swap live between the placeholder and the real content it's wrapping — "Reveal" flips `loading` to `false`, "Reset" flips it back.

<Preview align="start" code="&#x22;function Demo() {\n  const [loading, setLoading] = useState(true);\n  return (\n    <>\n      <Button size=\&#x22;sm\&#x22; variant=\&#x22;light\&#x22; color=\&#x22;neutral\&#x22; onClick={() => setLoading((v) => !v)}>\n        {loading ? 'Reveal' : 'Reset'}\n      </Button>\n      <Skeleton loading={loading}>\n        <span className=\&#x22;text-sm\&#x22;>Real content, revealed once loaded</span>\n      </Skeleton>\n    </>\n  );\n}&#x22;">
  <WrapModeDemo />
</Preview>

The overlay's `shape`/`size` still come from those props, same as standalone mode — only the placeholder's *dimensions* are inferred from the child, not its shape (a circular avatar's wrapper won't automatically become a circular placeholder; pass `shape="circular"` for that too).

The wrapper shrink-wraps to the content's own intrinsic size — content that itself stretches to fill its container (`w-full`, `flex-1`, an unconstrained block paragraph) won't get a full-width overlay from wrap mode alone. Add `className="block w-full"` in that case, or fall back to standalone mode with an explicit `width`.

## Recipes [#recipes]

Skeleton has no layout logic of its own — complex, card-style loading states are built by composing plain shapes inside the kit's existing [`Card`](/docs/card) and [`Flex`](/docs/flex)/[`Grid`](/docs/grid). This is exactly the pattern `DataTable`'s stacked-card loading view uses internally.

<Preview align="block" title="Rectangle" code="&#x22;<Skeleton size=\&#x22;sm\&#x22; />\n<Skeleton size=\&#x22;md\&#x22; className=\&#x22;w-2/3\&#x22; />&#x22;">
  <div className="flex w-full max-w-64 flex-col gap-2">
    <Skeleton size="sm" />

    <Skeleton size="md" className="w-2/3" />
  </div>
</Preview>

<Preview align="block" title="Circle" code="`<Skeleton shape=&#x22;circular&#x22; width={40} height={40} />`">
  <Skeleton shape="circular" width="40" height="40" />
</Preview>

<Preview align="block" title="Card stack — DataTable's own stacked skeleton pattern" code="&#x22;<Card padding=\&#x22;sm\&#x22;>\n  <Flex gap=\&#x22;sm\&#x22; align=\&#x22;center\&#x22;>\n    <Skeleton shape=\&#x22;circular\&#x22; width={40} height={40} />\n    <Flex direction=\&#x22;col\&#x22; gap=\&#x22;xs\&#x22; className=\&#x22;flex-1\&#x22;>\n      <Skeleton size=\&#x22;sm\&#x22; className=\&#x22;w-1/3\&#x22; />\n      <Skeleton size=\&#x22;xs\&#x22; className=\&#x22;w-2/3\&#x22; />\n    </Flex>\n  </Flex>\n</Card>&#x22;">
  <Card padding="sm" className="w-full max-w-80">
    <Flex gap="sm" align="center">
      <Skeleton shape="circular" width="40" height="40" />

      <Flex direction="col" gap="xs" className="flex-1">
        <Skeleton size="sm" className="w-1/3" />

        <Skeleton size="xs" className="w-2/3" />
      </Flex>
    </Flex>
  </Card>
</Preview>

## API [#api]

<TypeTable
  type="{
  shape: {
    type: &#x22;'rectangular' | 'circular'&#x22;,
    default: &#x22;'rectangular'&#x22;,
    description: &#x22;Placeholder silhouette.&#x22;,
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description:
      &#x22;Height (rectangular) or diameter (circular), off the kit's shared size scale.&#x22;,
  },
  width: {
    type: &#x22;string | number&#x22;,
    description:
      &#x22;Fixed width. Standalone mode only (no `children`) — wrap mode sizes off the real content instead.&#x22;,
  },
  height: {
    type: &#x22;string | number&#x22;,
    description: &#x22;Fixed height. Standalone mode only — see `width`.&#x22;,
  },
  animate: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description: &#x22;Plays the pulse animation.&#x22;,
  },
  loading: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description:
      &#x22;Standalone (no children): false renders nothing. Wrap mode (children set): false reveals the real children.&#x22;,
  },
  children: {
    type: &#x22;ReactNode&#x22;,
    description:
      &#x22;Wrap mode: real content to size the placeholder against, revealed once `loading` is false. Omit for a standalone fixed-shape placeholder.&#x22;,
  },
  testId: {
    type: &#x22;string&#x22;,
    description:
      &#x22;Renders as `data-testid`. Lands on the outer wrapper in wrap mode (`children` set), or on the placeholder `<div>` itself in standalone mode — see [Testing](/docs/testing).&#x22;,
  },
  classNames: {
    type: &#x22;{ skeleton?: string }&#x22;,
    description:
      &#x22;The placeholder box itself in wrap mode, separate from the wrapper's own className.&#x22;,
  },
}"
/>

All other props are forwarded to the root `<div>`.

## Accessibility [#accessibility]

A standalone placeholder is `aria-hidden` — it's a visual stand-in with nothing for assistive tech to announce. In wrap mode, the wrapper carries `aria-busy`/`data-loading` while loading, and the real (still-mounted, invisible) content is hidden from assistive tech until it's revealed. The pulse animation respects `prefers-reduced-motion` (`motion-reduce:animate-none`) — an improvement `DataTable`'s own pre-existing skeleton bars didn't have before adopting this component.
