Skeleton
Holds the space of content that is still loading. Use it for rows, titles, panels, and table cells, so the page does not jump when data arrives.
Shapes
Six footprints, each the size of the content it stands in for. Line is the default.
shape="line"16px tall, full width. Chat, file, and project rows.shape="title"24px tall, two thirds wide. Plan and canvas headings.shape="avatar"32px circle. Member and account pictures.shape="panel"128px tall, full width. Workflow diagrams and invitations.shape="checkbox"16px box. Table row selection.shape="action"28px square. Row edit and delete buttons.Loading lists
SkeletonGroup is a status region. Its label names what is loading and is read as hidden text. Gap is 8, 12, or 16px.
gap="sm"gap="md"gap="lg"Canvases and details
Title, line, and panel shapes build the outline of a canvas or a diagram before it loads.
Table rows
Checkbox and action shapes hold the selection and button cells. Hide an action skeleton when the person cannot use that action.
| Select | Crop | Bed | Sown | Harvest | Actions |
|---|---|---|---|---|---|
With visible status text
When the wait needs words, show them. Leave the group without a label and put the sentence inside it.
Loading GitHub accounts…
Checking who in “Greenhouse rotation” can open them…
Loading deliveries…
In cards and rows
Skeletons fill the body of a bordered card, or sit beside an icon in an event row.
Sidebar menu rows
className and style pass through. A small icon square and a text bar capped by a width variable stand in for each menu row.
Load states
The skeleton keeps the list height while it loads, then gives way to rows, an empty state, or an error with a retry.
Usage
import { Skeleton, SkeletonGroup } from "@tulipfarm/sprout/skeleton";
<SkeletonGroup label="Loading projects" gap="lg">
<Skeleton />
<Skeleton />
<Skeleton />
</SkeletonGroup>
<SkeletonGroup label="Loading workflow diagram">
<Skeleton shape="title" />
<Skeleton shape="panel" />
</SkeletonGroup>Skeleton renders an empty div. shapesets its size:
"line", the default: 16px tall, full width."title": 24px tall, two thirds wide."avatar": a 32px circle."panel": 128px tall, full width."checkbox": a 16px box."action": a 28px square, the size of a small icon button.
className, style, and data-*attributes pass through, so you can set a custom size or a width variable for one placeholder.
SkeletonGroup stacks skeletons in a statusregion. gap is "sm" (8px), "md"(12px, the default), or "lg" (16px). Use renderto change the element, for example to a section.
Table rows
<Table aria-label="Records" data={rows} columns={columns} getRowId={(row) => row.id} loading={isLoading} />Visible status text
<SkeletonGroup gap="sm">
<p>Loading GitHub accounts…</p>
<Skeleton />
<Skeleton />
</SkeletonGroup>When the wait needs words, put them inside the group and leave outlabel. The sentence then names the region.
Menu rows
<div className="flex h-8 items-center gap-2 px-2">
<Skeleton className="size-4 rounded-md" />
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
style={{ "--skeleton-width": "72%" } as CSSProperties}
/>
</div>Accessibility
Each skeleton is aria-hidden, so screen readers skip it.SkeletonGroup has role="status". Itslabel becomes the accessible name and is also rendered as visually hidden text, such as “Loading projects…”. Skeletons are not focusable and ignore the pointer.
The pulse runs every 1.6 seconds and stops when reduced motion is requested. In forced colors mode each skeleton gets a visible border.
Theme tokens
The fill is --sprout-secondary. Corners follow--sprout-radius-control.