Accessibility pattern · Feedback and status

Skeleton loader

The grey shapes are for eyes only: they are aria-hidden, sized like the real cards so nothing jumps, and they stop shimmering when the system asks for less motion. Screen readers get the same story in words: a list marked busy, a hidden Loading profiles status, and one polite message when the people arrive.

WCAG criteria
3
Keyboard rules
2
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Design team

Pune and Bengaluru studios

Loading profiles

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabReaches Reload; the placeholders take no tab stop and are not read.
Enter or SpaceOn Reload, shows the placeholders again and loads the team; focus stays on the button.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Reading reaches the list while it loadsDesign team, list, busy. Loading profiles
The profiles arrive4 profiles loaded
Reload is pressedLoading profiles, then 4 profiles loaded
Reading reaches a placeholderNothing: placeholders are aria-hidden

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-skeleton" data-ap-skeleton>
  <div class="ap-skeleton__top">
    <div>
      <h3 class="ap-skeleton__h" id="skeleton-h">Design team</h3>
      <p class="ap-skeleton__sub">Pune and Bengaluru studios</p>
    </div>
    <button type="button" class="ap-btn" data-ap-reload>
      <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 11a8 8 0 0 0-14.3-4.9L4 8"/><path d="M4 4v4h4"/><path d="M4 13a8 8 0 0 0 14.3 4.9L20 16"/><path d="M20 20v-4h-4"/></svg>
      Reload
    </button>
  </div>

  <ul class="ap-skeleton__list" aria-labelledby="skeleton-h" aria-busy="true" data-ap-list>
      <li class="ap-skeleton__card ap-skeleton__card--ghost" aria-hidden="true">
        <span class="ap-skeleton__avatar ap-skeleton__bone"></span>
        <div class="ap-skeleton__body">
          <p class="ap-skeleton__name"><span class="ap-skeleton__bone ap-skeleton__bone--60"></span></p>
          <p class="ap-skeleton__role"><span class="ap-skeleton__bone ap-skeleton__bone--80"></span></p>
          <p class="ap-skeleton__place"><span class="ap-skeleton__bone ap-skeleton__bone--40"></span></p>
          <p class="ap-skeleton__tags"><span class="ap-skeleton__tag ap-skeleton__bone"></span> <span class="ap-skeleton__tag ap-skeleton__bone ap-skeleton__bone--wide"></span></p>
        </div>
      </li>
      <li class="ap-skeleton__card ap-skeleton__card--ghost" aria-hidden="true">
        <span class="ap-skeleton__avatar ap-skeleton__bone"></span>
        <div class="ap-skeleton__body">
          <p class="ap-skeleton__name"><span class="ap-skeleton__bone ap-skeleton__bone--60"></span></p>
          <p class="ap-skeleton__role"><span class="ap-skeleton__bone ap-skeleton__bone--80"></span></p>
          <p class="ap-skeleton__place"><span class="ap-skeleton__bone ap-skeleton__bone--40"></span></p>
          <p class="ap-skeleton__tags"><span class="ap-skeleton__tag ap-skeleton__bone"></span> <span class="ap-skeleton__tag ap-skeleton__bone ap-skeleton__bone--wide"></span></p>
        </div>
      </li>
      <li class="ap-skeleton__card ap-skeleton__card--ghost" aria-hidden="true">
        <span class="ap-skeleton__avatar ap-skeleton__bone"></span>
        <div class="ap-skeleton__body">
          <p class="ap-skeleton__name"><span class="ap-skeleton__bone ap-skeleton__bone--60"></span></p>
          <p class="ap-skeleton__role"><span class="ap-skeleton__bone ap-skeleton__bone--80"></span></p>
          <p class="ap-skeleton__place"><span class="ap-skeleton__bone ap-skeleton__bone--40"></span></p>
          <p class="ap-skeleton__tags"><span class="ap-skeleton__tag ap-skeleton__bone"></span> <span class="ap-skeleton__tag ap-skeleton__bone ap-skeleton__bone--wide"></span></p>
        </div>
      </li>
      <li class="ap-skeleton__card ap-skeleton__card--ghost" aria-hidden="true">
        <span class="ap-skeleton__avatar ap-skeleton__bone"></span>
        <div class="ap-skeleton__body">
          <p class="ap-skeleton__name"><span class="ap-skeleton__bone ap-skeleton__bone--60"></span></p>
          <p class="ap-skeleton__role"><span class="ap-skeleton__bone ap-skeleton__bone--80"></span></p>
          <p class="ap-skeleton__place"><span class="ap-skeleton__bone ap-skeleton__bone--40"></span></p>
          <p class="ap-skeleton__tags"><span class="ap-skeleton__tag ap-skeleton__bone"></span> <span class="ap-skeleton__tag ap-skeleton__bone ap-skeleton__bone--wide"></span></p>
        </div>
      </li>
  </ul>
  <p class="ap-skeleton__sr" role="status" data-ap-say>Loading profiles</p>

  <template data-ap-people>
    <li class="ap-skeleton__card">
      <span class="ap-skeleton__avatar ap-skeleton__avatar--a" aria-hidden="true">AR</span>
      <div class="ap-skeleton__body">
        <p class="ap-skeleton__name">Asha Rao</p>
        <p class="ap-skeleton__role">Lead product designer</p>
        <p class="ap-skeleton__place"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21Z"/><circle cx="12" cy="9.5" r="2.5"/></svg><span>Pune</span></p>
        <p class="ap-skeleton__tags"><span class="ap-skeleton__tag">Figma</span> <span class="ap-skeleton__tag">Design systems</span></p>
      </div>
    </li>
    <li class="ap-skeleton__card">
      <span class="ap-skeleton__avatar ap-skeleton__avatar--b" aria-hidden="true">VN</span>
      <div class="ap-skeleton__body">
        <p class="ap-skeleton__name">Vikram Nair</p>
        <p class="ap-skeleton__role">UX researcher</p>
        <p class="ap-skeleton__place"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21Z"/><circle cx="12" cy="9.5" r="2.5"/></svg><span>Bengaluru</span></p>
        <p class="ap-skeleton__tags"><span class="ap-skeleton__tag">Interviews</span> <span class="ap-skeleton__tag">Surveys</span></p>
      </div>
    </li>
    <li class="ap-skeleton__card">
      <span class="ap-skeleton__avatar ap-skeleton__avatar--c" aria-hidden="true">ZK</span>
      <div class="ap-skeleton__body">
        <p class="ap-skeleton__name">Zoya Khan</p>
        <p class="ap-skeleton__role">Visual designer</p>
        <p class="ap-skeleton__place"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21Z"/><circle cx="12" cy="9.5" r="2.5"/></svg><span>Pune</span></p>
        <p class="ap-skeleton__tags"><span class="ap-skeleton__tag">Illustration</span> <span class="ap-skeleton__tag">Motion</span></p>
      </div>
    </li>
    <li class="ap-skeleton__card">
      <span class="ap-skeleton__avatar ap-skeleton__avatar--d" aria-hidden="true">DM</span>
      <div class="ap-skeleton__body">
        <p class="ap-skeleton__name">Dev Malhotra</p>
        <p class="ap-skeleton__role">Accessibility lead</p>
        <p class="ap-skeleton__place"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21Z"/><circle cx="12" cy="9.5" r="2.5"/></svg><span>Bengaluru</span></p>
        <p class="ap-skeleton__tags"><span class="ap-skeleton__tag">WCAG</span> <span class="ap-skeleton__tag">Audits</span></p>
      </div>
    </li>
  </template>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The placeholders are hidden from assistive technology, so the list exposes only real people; while loading it is marked busy and named by its heading.

  • 2.3.3 Animation from Interactions Level AAA

    The shimmer stops when the system asks for reduced motion; the placeholders stay, still and grey.

  • 4.1.3 Status Messages Level AA

    A visually hidden status says Loading profiles and then how many arrived, once, without moving focus.

Usage

When to use it

Use it

  • Content with a known shape that takes a moment to load: cards, lists, profiles, feeds.
  • First loads of a page or panel, where a blank space or a jumping layout would be worse.

Use something else

  • Short waits on an action someone just took, like a save: a busy button says more.
  • Content whose shape you cannot predict: placeholders that do not match make the layout jump anyway.
  • Long tasks with a known size: show a progress bar.

Common failures

How it usually goes wrong

  • Placeholders read aloud

    Empty boxes exposed to a screen reader become a list of blank items, or a string of "group". The placeholders here are aria-hidden.

  • Silence until it is done

    Hiding the placeholders without saying anything leaves screen reader users with an empty list. A hidden status says Loading profiles, then how many arrived.

  • One message per card

    Announcing each card as it arrives reads the whole team aloud. One message gives the count; people read the list when they want.

  • Shapes that do not match

    Placeholders shorter than the real cards make the page jump when content lands, and people lose their place or misclick. These share the real cards' sizes.

  • A shimmer that never stops

    Constant shimmer is motion some people cannot tolerate. It stops under reduced motion and ends as soon as the content arrives.

Notes

Building it

  • The status element is in the HTML from the start, holding Loading profiles. A live region announces changes only once the browser knows about it; a status added together with its words, or unhidden as they change, is often silent.
  • aria-busy="true" on the list says its contents are being replaced. Screen readers treat it differently, so the status message carries the news.
  • The placeholders use the real cards' grid, avatar size and line heights, with each line holding one bar, so the cards land in exactly the same space.
  • Reload while loading starts again: the placeholders return, the status says Loading profiles, and the timer restarts.
  • The data is simulated; the Slow network option keeps the placeholders on screen for five seconds.

Sources: WAI-ARIA 1.2: aria-busy · Understanding SC 4.1.3: Status Messages · Understanding SC 2.3.3: Animation from Interactions

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction