ExtrasSkeleton

Placeholder lines, avatars and blocks shown while content loads, driven by aria-busy.

On this page

A container with aria-busy="true" shows its empty data-skeleton children as placeholders. The values are line, avatar and block. Set the width of a line with inline-size. When the data arrives, set aria-busy="false" or swap the content.

Load css/extras/skeleton.min.css, or the whole set from css/pico.extras.min.css. See the overview.

A container that holds skeletons does not show the core aria-busy spinner. A container with aria-busy="true" and no skeletons keeps it (for example <p aria-busy="true">Saving your changes</p>).

Card

Give the skeleton the shape of the content it stands in for: one line per line of text, an avatar only where the loaded card has an image. When the two match, the swap does not move the page.

Loading card
<article aria-busy="true" aria-label="Loading invoice">
  <header>
    <p data-skeleton="line" style="inline-size: 40%"></p>
  </header>
  <p data-skeleton="line"></p>
  <footer>
    <p data-skeleton="line" style="inline-size: 30%"></p>
  </footer>
</article>

The same card once the data is in, with aria-busy="false":

HTML
<article aria-busy="false">
  <header>
    <div style="display: flex; align-items: center; gap: var(--pico-spacing)">
      <strong>Acme Cloud</strong>
      <small>Invoice 2041, paid</small>
    </div>
  </header>
  <p>Your team plan renewed on 1 October. The next invoice is due on 1 November.</p>
  <footer><small>Billing contact: billing@acme.example</small></footer>
</article>

Text lines

Use a role="status" container with an aria-label so screen readers announce the loading state and the finished content.

Loading text
<section role="status" aria-busy="true" aria-label="Loading comments">
  <p data-skeleton="line"></p>
  <p data-skeleton="line" style="inline-size: 90%"></p>
  <p data-skeleton="line" style="inline-size: 70%"></p>
</section>

Avatar and lines

Loading profile
<div
  role="status"
  aria-busy="true"
  aria-label="Loading profile"
  style="display: flex; align-items: center; gap: var(--pico-spacing)"
>
  <span data-skeleton="avatar"></span>
  <div style="flex: 1">
    <p data-skeleton="line" style="inline-size: 50%"></p>
    <p data-skeleton="line" style="inline-size: 30%"></p>
  </div>
</div>

Results swapped by htmx

A results list swapped by htmx follows the same pattern. The list starts busy with three lines, and htmx replaces the inner HTML when the response arrives. The new content has no skeletons, so set aria-busy="false" on the response, or let your code clear the attribute after the swap.

HTML
<section
  id="results"
  aria-busy="true"
  aria-label="Search results"
  hx-get="/customers"
  hx-trigger="keyup changed delay:300ms from:input[name=q]"
  hx-target="this"
  hx-swap="innerHTML"
>
  <p data-skeleton="line"></p>
  <p data-skeleton="line" style="inline-size: 75%"></p>
  <p data-skeleton="line" style="inline-size: 55%"></p>
</section>

Accessibility

  • aria-busy="true" tells assistive technology the region is still updating. Name the region with aria-label so it is clear what is loading.
  • The placeholders are empty and have no text, so there is nothing for a screen reader to read.
  • The shimmer only runs under prefers-reduced-motion: no-preference. Everyone else gets the flat color.

Customization

--pico-skeleton-color
var(--pico-muted-border-color)
Base color of a placeholder.
--pico-skeleton-highlight-color
light-dark( var(--pico-form-element-background-color), oklch(0.32 0.008 265) )
Color of the moving highlight.
--pico-skeleton-duration
1.5s
Time for one sweep of the shimmer.
--pico-skeleton-line-height
1em
Height of a line.
--pico-skeleton-avatar-size
3rem
Diameter of an avatar placeholder.
--pico-skeleton-block-size
8rem
Height of a block.