Flux 2.0

Component

Skeleton

Flat placeholder shapes that hold the layout of content that is still loading.

Plain-text source, for agents and clipboard use: /components/source/skeleton.txt

When to use it

Shows the shape of what is coming when its layout is already known: a card, a list, the rows of a table. Each text line occupies exactly the line box of the text it stands in for, so the content replaces it without the page moving. The shapes pulse slowly in a layer fill, with no shimmer and no gradient, and the region says in words what is loading.

Not this
Not when the layout is not known in advance, such as search results that may be one row or none; use a Spinner. Not for a single value or control; use a small Spinner inside it. Not for anything that loads in under half a second.

Examples

Text

One flux-skeleton--text per line of the text that is coming. The last line of a paragraph runs short, so it reads as prose.

html
<div aria-hidden="true">
  <span class="flux-skeleton flux-skeleton--text"></span>
  <span class="flux-skeleton flux-skeleton--text"></span>
  <span class="flux-skeleton flux-skeleton--text"></span>
</div>

Card

The card is the region. The title slot takes flux-card__title, so the line is the height of the heading that replaces it.

Loading departure
html
<div class="flux-card" aria-busy="true">
  <span class="flux-visually-hidden" role="status">Loading departure</span>
  <div class="flux-card__title" aria-hidden="true"><span class="flux-skeleton flux-skeleton--text flux-skeleton--short"></span></div>
  <div class="flux-card__body" aria-hidden="true">
    <span class="flux-skeleton flux-skeleton--text"></span>
    <span class="flux-skeleton flux-skeleton--text"></span>
  </div>
</div>

List with avatars

flux-skeleton-row sets an avatar beside its lines: a name and a line of metadata.

Loading guides
html
<div aria-busy="true">
  <span class="flux-visually-hidden" role="status">Loading guides</span>
  <div aria-hidden="true">
    <div class="flux-skeleton-row">
      <span class="flux-skeleton flux-skeleton--avatar"></span>
      <div>
        <span class="flux-skeleton flux-skeleton--text flux-skeleton--short"></span>
        <span class="flux-skeleton flux-skeleton--text"></span>
      </div>
    </div>
    <div class="flux-skeleton-row">
      <span class="flux-skeleton flux-skeleton--avatar"></span>
      <div>
        <span class="flux-skeleton flux-skeleton--text flux-skeleton--short"></span>
        <span class="flux-skeleton flux-skeleton--text"></span>
      </div>
    </div>
    <div class="flux-skeleton-row">
      <span class="flux-skeleton flux-skeleton--avatar"></span>
      <div>
        <span class="flux-skeleton flux-skeleton--text flux-skeleton--short"></span>
        <span class="flux-skeleton flux-skeleton--text"></span>
      </div>
    </div>
  </div>
</div>

Table rows

The headers are real, because the columns are known; only the body waits. A cell’s line is inline, so it follows the cell’s alignment, including numeric columns.

Loading bookings
Bookings departing in September
Reference Departure Status Pax Total
html
<div class="flux-table-wrap" aria-busy="true">
  <span class="flux-visually-hidden" role="status">Loading bookings</span>
  <table class="flux-table">
    <caption class="flux-visually-hidden">Bookings departing in September</caption>
    <thead>
      <tr>
        <th scope="col">Reference</th>
        <th scope="col">Departure</th>
        <th scope="col">Status</th>
        <th scope="col" data-numeric>Pax</th>
        <th scope="col" data-numeric>Total</th>
      </tr>
    </thead>
    <tbody aria-hidden="true">
      <tr>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
      </tr>
      <tr>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
      </tr>
      <tr>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
        <td data-numeric><span class="flux-skeleton flux-skeleton--text"></span></td>
      </tr>
    </tbody>
  </table>
</div>

Block

flux-skeleton--block holds the place of a chart, a map or an image. To match what is coming, set --flux-skeleton-block-size on the region to another spacing token.

Loading occupancy chart
html
<div class="flux-card" aria-busy="true">
  <span class="flux-visually-hidden" role="status">Loading occupancy chart</span>
  <div class="flux-card__title" aria-hidden="true"><span class="flux-skeleton flux-skeleton--text flux-skeleton--short"></span></div>
  <span class="flux-skeleton flux-skeleton--block" aria-hidden="true"></span>
</div>

Anatomy

PartDescription
RegionThe element that will hold the content, with aria-busy="true" until it arrives.
StatusA role="status" element naming what is loading, usually flux-visually-hidden.
SlotAn aria-hidden element carrying the class of the text it replaces, such as flux-card__title, so the line takes that text’s size and line height.
Shapeflux-skeleton with a modifier: a text line, an avatar or a block.

API

NameKindDescription
flux-skeletonclassA shape. Always inside an aria-hidden element: the status carries the meaning.
flux-skeleton--textclassOne line, at cap height in the line box of the surrounding text. The last of several runs short.
flux-skeleton--shortclassA short line, for a title or a name.
flux-skeleton--avatarclassA circle the size of a medium Avatar.
flux-skeleton--blockclassA rectangle --flux-skeleton-block-size tall, for a chart, map or image.
flux-skeleton-rowclassAn avatar beside a group of lines.
aria-busy="true"attributeOn the region, removed when the content replaces the skeleton.

States

StateTreatment
LoadingThe shapes pulse to half strength and back once every --flux-skeleton-duration.
Reduced motionThe base layer runs the pulse once and it ends at full strength, so the shapes rest as still fills.
LoadedThe content replaces the skeleton in place, and aria-busy is removed.

Keyboard

KeysAction
—Not interactive. Nothing inside a skeleton takes focus.

Accessibility

  • Put aria-busy="true" on the region and a role="status" element inside it that names what is loading.
  • Mark every slot and shape aria-hidden. Empty headings or cells announced one by one say nothing useful.
  • Keep real column headers in a loading table, so the structure is announced and only the data waits.
  • The fill is a graphic with no text, so it relies on the status for meaning rather than on its contrast.

Tokens

The tier-3 and tier-2 tokens this component binds to. Change the token, not the component.

  • --flux-skeleton-bg | -radius
  • --flux-skeleton-block-size | -avatar-size
  • --flux-skeleton-duration | -ease | -dim

Do and don’t

Do

  • Draw the layout that is actually coming: the same number of columns, a believable number of rows.
  • Wait about half a second before showing a skeleton, so a fast response never flashes one.
  • Replace the skeleton with a message and a retry action if loading fails.

Don’t

  • Do not add a shimmer, a gradient sweep or a glow.
  • Do not show a skeleton and a Spinner for the same region.
  • Do not skeleton what is already known, such as section headings, field labels and column headers.