Skeleton
A neutral grey placeholder block to render in place of content while it loads. Use it for lists, cards, and panels that need to feel responsive before the data arrives — keeps the visual language consistent across loading states.
Live Preview
When to use
- Skeleton — content has a known shape (rows, avatars, cards) and you want to mirror that layout while loading.
- Spinner — generic activity indicator with no layout to mirror; pair with a label or use inside a button.
- EmptyState — the request completed and returned nothing, or you have not yet asked for data.
Reach for Skeleton when the placeholder structure is itself information (“there will be five rows here”). Reach for Spinner when the placeholder is just “we’re working on it”.
Import
import { Skeleton, SkeletonGroup, SkeletonLayout } from 'entangle-ui';Usage
<Skeleton width={120} height={16} /><Skeleton shape="circle" width={32} /><Skeleton shape="line" width="60%" />Shapes
Shapes
| Shape | Default radius | Default height | Use for |
|---|---|---|---|
rect | borderRadius.sm | 100% of container | Thumbnails, cards, blocks |
circle | 50% | matches width | Avatars, dots, badge slots |
line | borderRadius.lg | 12px | Single text-line placeholders |
<Skeleton shape="rect" width={120} height={48} /><Skeleton shape="circle" width={48} /><Skeleton shape="line" width="60%" />Animations
Animations
| Animation | Behavior | Use when |
|---|---|---|
pulse | Opacity oscillates between 1 and 0.4 at 1.5 s | Default — works almost everywhere |
wave | Shimmer gradient sweeps left-to-right at 1.6 s | Hero placeholders, cards above the fold |
none | Static base color | 20+ skeletons on screen at once |
prefers-reduced-motion: reduce automatically suppresses both animations — no code changes needed.
<Skeleton animation="pulse" /><Skeleton animation="wave" /><Skeleton animation="none" />Paragraph composition
Stack several shape="line" skeletons with varying widths to imitate a paragraph of text. Tail with a shorter line so the block reads as a real paragraph.
Paragraph
<Stack spacing={2}> <Skeleton shape="line" width="60%" /> <Skeleton shape="line" width="100%" /> <Skeleton shape="line" width="90%" /> <Skeleton shape="line" width="75%" /> <Skeleton shape="line" width="40%" /></Stack>Card composition
Mirror the real layout: avatar circle, a few title / description lines, and a thumbnail rect. The closer the skeleton matches the final shape, the less visual “snap” the user perceives when content lands.
Card
<Flex gap={3} align="flex-start"> <Skeleton shape="circle" width={40} /> <Stack spacing={2} style={{ flex: 1 }}> <Skeleton shape="line" width="70%" /> <Skeleton shape="line" width="100%" /> <Skeleton shape="line" width="50%" /> <Skeleton width="100%" height={80} /> </Stack></Flex>SkeletonGroup
SkeletonGroup arranges multiple skeletons consistently. Pass count for an auto-generated list (configured via itemProps), or pass children for a custom composition. direction and spacing control the flex container.
List
<SkeletonGroup count={5} spacing={2} itemProps={{ shape: 'line', width: '100%' }}/>Grid loading state
For dense surfaces (asset browser, gallery, file picker), drop animation entirely — running 12 + animations at once is more noise than help.
Grid
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 80px)', gap: 12 }}> {Array.from({ length: 12 }, (_, i) => ( <Skeleton key={i} width={80} height={80} animation="none" /> ))}</div>SkeletonLayout
SkeletonLayout is a pre-built alternative to manual composition. Pick a variant and the component renders a structured arrangement of Skeleton blocks — handy when the surrounding UI shape is well-known (a card, a feed, a data grid) and you don’t want to wire up the placeholder by hand.
<SkeletonLayout variant="card" /><SkeletonLayout variant="list" count={8} /><SkeletonLayout variant="table" columns={5} count={6} /><SkeletonLayout variant="grid" columns={3} count={9} /><SkeletonLayout variant="chat" count={6} />Card
List
Table
Grid
Chat
| Variant | Default count | Default columns | Default animation |
|---|---|---|---|
card | 1 (single unit) | — | pulse |
list | 5 rows | — | pulse |
table | 5 rows + header | 4 | pulse |
grid | 12 cells | 4 | none |
chat | 4 bubbles | — | pulse |
grid defaults to animation="none" because dense grids commonly render dozens of blocks at once; the static treatment reads as “loading” just as clearly without spending GPU cycles. Override animation on any variant to opt in or out.
In an editor panel
Use shape="line" skeletons inside PropertySection to reflect the panel’s editor density while data loads.
Property panel
<PropertyPanel size="sm"> <PropertySection title="Transform" defaultExpanded> <Stack spacing={2}> <Skeleton shape="line" width="100%" /> <Skeleton shape="line" width="80%" /> </Stack> </PropertySection></PropertyPanel>Performance
Each animated skeleton runs a CSS animation on the GPU, so a handful is cheap. When you have 20 + skeletons on screen at once (grid views, large lists), prefer animation="none" — the eye reads the static blocks as “loading” just as well, and the page stays smooth.
Accessibility
- Skeletons are decorative: they set
aria-hidden="true"andaria-busy="true"but do not announce themselves. - The surrounding container is responsible for announcing loading state — pair with a
Spinneror anEmptyState loadingif assistive tech needs to be informed. - Animation is automatically halted under
prefers-reduced-motion: reduce; the wave gradient is also suppressed.
API Reference
<Skeleton>
| Prop | Type | Default | Description |
|---|---|---|---|
shape | 'rect' | 'circle' | 'line' | 'rect' | Visual shape. Determines the default border radius and the height fallback for `line`. |
width | number | string | '100%' | Width. Number → px, string → CSS value. |
height | number | string | — | Height. Number → px, string → CSS value. Defaults to 12px for `line`; matches `width` for `circle`. |
borderRadius | number | string | — | Override the radius. Defaults are derived from the shape. |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | Animation style. Both pulse and wave honor `prefers-reduced-motion`. |
className | string | — | Additional CSS class names. |
style | CSSProperties | — | Inline styles. |
testId | string | — | Test identifier for automated testing. |
ref | Ref<HTMLDivElement> | — | Ref to the underlying div element. |
<SkeletonGroup>
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | — | Number of skeletons to auto-generate. Ignored when `children` is provided. |
spacing | number | string | 2 | Number maps onto the spacing scale (0 → 0, 1 → xs, 2 → sm, 3 → md, 4 → lg, 5 → xl, 6 → xxl, 7+ → xxxl). String passes through as a raw CSS gap. |
direction | 'row' | 'column' | 'column' | Flex layout direction. |
itemProps | Partial<SkeletonProps> | — | Props applied to each auto-generated `Skeleton` (used only with `count`). |
children | ReactNode | — | When provided, overrides `count` and the wrapper just lays the children out. |
className | string | — | Additional CSS class names. |
style | CSSProperties | — | Inline styles. |
testId | string | — | Test identifier for automated testing. |
ref | Ref<HTMLDivElement> | — | Ref to the underlying div element. |
<SkeletonLayout>
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'card' | 'list' | 'table' | 'grid' | 'chat' | — | Which pre-built arrangement to render. Required. |
count | number | — | Number of repeating units. Defaults per variant: `list` 5 rows, `table` 5 data rows, `grid` 12 cells, `chat` 4 bubbles. Ignored for `card`. |
columns | number | 4 | Number of columns. Only meaningful for `grid` and `table`. |
animation | 'pulse' | 'wave' | 'none' | — | Animation applied to every nested skeleton. Defaults to `pulse` for `card` / `list` / `table` / `chat`, and `none` for `grid`. |
width | number | string | '100%' | Width of the layout container. Number → px, string → CSS value. Defaults to filling the parent so the layout stays stable while content loads. |