Skeleton

A placeholder for content that is still loading. It keeps the shape of what it replaces and shimmers with the same light sweep as Marker’s text.

Installation

import {
  Skeleton,
  SkeletonGroup,
} from "@o/pipeline/experimental/components/skeleton";

Leaves, not layout

Skeleton only the content that is loading. Render containers for real straight away, such as a Card, a table’s frame and headers, or a list’s frame, and let only the content inside them show as placeholders. Layout components take no skeleton prop. Neither do on-demand popups whose content is already on screen, such as Find, Menu and Tooltip: they open over a loaded page, so there is nothing for a placeholder to stand in for.

Note

With reduced motion, and in Firefox, the fill doesn’t shimmer.

skeleton

Pass skeleton to a component instead of swapping it for a hand-made placeholder, so its box keeps its shape when the content arrives. The swap is instant, with no fade. These components take it:

<Table.Cell skeleton={loading}>{company.name}</Table.Cell>

A skeleton is inert: nothing in it takes focus or reaches assistive technology.

  • JB
  • OP
  • Active
  • 12
  • Renewal
  • Renewal
  • K
  • ⌘K
CompanyStage
A
AL

Skeleton

Use Skeleton for content that no component stands in for, such as a row’s text or a whole card. By default, it takes the size of what you pass it, without showing it:

  • Text: gets a bar over each line it wraps to, as wide as that line. Each bar runs from the top of the tallest letters to the tail of a g, centred on the line by the font’s metrics (see Typography).
  • An element: gives the skeleton its width and height.
  • Nothing: needs a width. skeletonLineWidth(row, column) varies the width by position and returns the same value on every render.

A width or className overrides the default size.

<Skeleton width={skeletonLineWidth(row)} />
<Skeleton shape="block" className="h-55" />

shape="block" fills its container’s width and takes its height from its content or its className. Use one block for a whole card, such as a chart, until it has leaves of its own.

shape="lines" does for any content what a line does for text: the content lays out unseen in the element its render and className give, and each line it sets gets a bar. Feed.Event uses it, so an event’s text parts read as one run of text.

SkeletonText

SkeletonText shows its text as a placeholder while its group is loading, or when you set its skeleton prop. Otherwise it renders the text as it is. Use it for text inside your own markup, such as the name beside an avatar.

<Table.Cell skeleton={loading}>
  <Avatar.Root>…</Avatar.Root>
  <SkeletonText>{company.name}</SkeletonText>
</Table.Cell>

SkeletonContent

SkeletonContent marks its content as placeholders without adding an element. Components inside it draw their own skeletons, and plain text becomes lines. Table.Cell uses it, so a cell with an avatar and a name shows the avatar’s shape and a bar for the name.

SkeletonGroup

SkeletonGroup sets skeleton for every component inside it, so a whole block loads at once. For text with no component of its own, read the group with useSkeleton().

<SkeletonGroup loading={loading}>
  <EntityList items={items} />
</SkeletonGroup>
AdGr

Nested groups

The innermost group decides which components draw a skeleton, and a component’s own skeleton prop overrides any group. A finished group inside a loading one shows its content, but you can’t use it until the outer group finishes too.

OuOWIts own prop beats the group
InInner groupShown once the inner group is done, usable once both are

SkeletonStatus

SkeletonStatus speaks for the loading groups inside it, for a region whose parts load separately, such as a card whose sections each sync on their own. While any of those groups is showing, it announces one status, its label, and the groups stay quiet. It adds no element, and it changes nothing else: each group still waits and paints as it would on its own, and the content around them stays real and usable.

<SkeletonStatus label="Loading workstream">
  <Header workstream={workstream} />
  <SkeletonGroup loading={!progress}>…</SkeletonGroup>
  <SkeletonGroup loading={!signals}>…</SkeletonGroup>
</SkeletonStatus>

Blocks

Each block exports a skeleton built from the real block inside a SkeletonGroup, so every place that waits on it shows the same shape:

  • TableSkeleton and TableSkeletonRow
  • EntityListSkeleton
  • TaskListSkeleton
  • SeriesChartSkeleton

Open deals

Daily signupsPeriod

The data-skeleton attribute

data-skeleton marks the element that paints a skeleton. skeleton.css hides what’s inside that element and paints the shimmering fill in its box, radius and all.

Style a component’s own skeleton state with the data-skeleton: variant, such as Glyph’s softer radius. The attribute always comes with inert.

A component gets both from useSkeleton(). The hook resolves the component’s skeleton prop against the nearest group, and returns props to spread or merge onto the element:

const { skeleton, props } = useSkeleton(skeletonProp);
return <span {...props}>…</span>;

Note

A component’s other data attributes come from useRender state. data-skeleton comes from these props instead, so every component sets it the same way, whether it’s built on useRender or not.

When the box that paints the skeleton isn’t the element that must be inert, the component sets the two separately from skeleton:

  • Checkbox: paints its inner box and makes its root inert, so the frame around the box stays clear
  • Ghost Button: has no box to stand in for, so a bar over its text and icons paints the skeleton, and the button itself is only inert
  • Table.Cell: is only inert, and the content inside it draws its own skeletons

Accessibility

Only the outermost loading group announces anything. It sets aria-busy and announces one status, its label (“Loading…” by default). Groups inside it stay quiet. Inside a SkeletonStatus, the status speaks for them instead.

A loading group is inert as a whole, so you can’t use anything in it, and assistive technology hears only its status. The status sits beside the group, not inside it. Links keep their hrefs.

The outermost group waits 150 ms before its skeletons show, so a fast load never flashes one. A group that takes over from a skeleton already on screen shows at once.