Skip to content

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

ShapeDefault radiusDefault heightUse for
rectborderRadius.sm100% of containerThumbnails, cards, blocks
circle50%matches widthAvatars, dots, badge slots
lineborderRadius.lg12pxSingle text-line placeholders
<Skeleton shape="rect" width={120} height={48} />
<Skeleton shape="circle" width={48} />
<Skeleton shape="line" width="60%" />

Animations

Animations

AnimationBehaviorUse when
pulseOpacity oscillates between 1 and 0.4 at 1.5 sDefault — works almost everywhere
waveShimmer gradient sweeps left-to-right at 1.6 sHero placeholders, cards above the fold
noneStatic base color20+ 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

VariantDefault countDefault columnsDefault animation
card1 (single unit)pulse
list5 rowspulse
table5 rows + header4pulse
grid12 cells4none
chat4 bubblespulse

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" and aria-busy="true" but do not announce themselves.
  • The surrounding container is responsible for announcing loading state — pair with a Spinner or an EmptyState loading if 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.