Navigation

v0.0.4
Feedback

Skeleton

The shape of content that is on its way.

On this page

A skeleton stands in for content that is on its way, in the shape it will have, so the page does not jump when it arrives. It is a chip-toned block that a soft band of light crosses. Skeletons in one block start one after another, so the light runs down the block as a single pass where the browser supports sibling-index(). Size each like the content it replaces. Content that is already there and is being fetched again is busy instead.

Skeleton

<div class="flex items-center gap-sm" aria-busy="true" aria-label="Loading member">  <span class="skeleton skeleton-circle w(40)" aria-hidden="true"></span>  <div class="flex flex-col w-full text-sm" aria-hidden="true">    <span class="skeleton skeleton-text max-w(160)"></span>    <span class="skeleton skeleton-text max-w(320)"></span>  </div></div>

#Shapes

  • skeleton alone is a block. Give it the height of an image, a chart or a card.
  • skeleton-text is one line of text: as tall as the letters and centered in the line it stands for, so the real text lands on the same baseline grid. It follows the font size and line height around it.
  • skeleton-circle is an avatar or an icon. Give it a width; the height follows.

A loading card

<article class="card max-w(320) mx-auto" aria-busy="true" aria-label="Loading project">  <span class="skeleton card-media" aria-hidden="true"></span>  <div class="flex flex-col" aria-hidden="true">    <span class="skeleton skeleton-text max-w(160)"></span>    <span class="skeleton skeleton-text"></span>    <span class="skeleton skeleton-text max-w(240)"></span>  </div></article>

#Accessibility

  • Hide skeletons with aria-hidden="true" and mark the region they fill with aria-busy="true" and a label that says what is loading.
  • With reduced motion the band of light does not move; the shape alone says the content is pending.
  • Show a skeleton only when a wait is likely to be noticed. For a short wait, show nothing; for a button, use aria-busy and a spinner.

#Classes

ClassDescription
skeletonA placeholder block; give it the height of the image, chart or card it stands for
skeleton-textOne line of text: as tall as the letters and centered in the line it stands for. With skeleton
skeleton-circleA circle for an avatar or an icon, sized by its width. With skeleton

#Source

@master/ui/components/skeleton.css
/* * Skeleton: the shape of content that is on its way. * A chip-toned block that a soft band of light crosses; with reduced motion it rests. * Hide it from assistive technology and mark the loading region with aria-busy. */@layer components {  .skeleton {    display: block;    min-height: .75rem;    border-radius: calc(var(--radius-chip) * var(--corner-scale));    corner-shape: var(--corner-shape);    background:      linear-gradient(100deg, transparent 35%, var(--color-sheen) 50%, transparent 65%) 150% 0 / 300% 100% no-repeat,      var(--color-surface-chip);    color: transparent;    user-select: none;    -webkit-user-select: none;    pointer-events: none;    animation: skeleton-sweep 1.8s var(--easing-smooth) infinite;    /* One line of text: the block is as tall as the letters, centered in the line it stands for. */    &.skeleton-text {      height: .75em;      min-height: 0;      margin-block: calc((1lh - .75em) / 2);      border-radius: calc((var(--radius-chip) - 2px) * var(--corner-scale));    }    &.skeleton-circle {      aspect-ratio: 1;      border-radius: 999px;      corner-shape: round;    }    /* Lines in one block start one after another, so the band of light runs down the block as a single pass. */    @supports (width: calc(sibling-index() * 1px)) {      animation-delay: calc((sibling-index() - 1) * 90ms);    }    @media (prefers-reduced-motion: reduce) {      animation: none;    }    @media (forced-colors: active) {      outline: 1px solid GrayText;      outline-offset: -1px;    }  }}@keyframes skeleton-sweep {  to {    background-position: -50% 0, 0 0;  }}