Accessible Loading States, Skeletons, and Spinners in React

Make loading UI accessible with a live region that announces progress, aria-busy, and decorative skeletons and spinners hidden from screen readers.

6 min read

Accessible loading states tell screen reader users when content is loading and when it is done, while keeping decorative skeletons and spinners out of their way. A status region announces the change, aria-busy holds it until the update finishes, and aria-hidden hides the animation.

Announce loading with a status region

A role of status creates a polite live region. When its text changes, screen readers announce it, so a loading message has somewhere to land.

App.jsxApp.jsx
export default function Status({ loading }) {
  return (
    <p role="status">
      {loading ? "Loading results" : "Results loaded"}
    </p>
  );
}

When loading flips to false, the paragraph text changes and the screen reader says "Results loaded." The status role implies aria-live="polite" and aria-atomic="true", so the whole message is announced at once.

Hold the update with aria-busy

Set aria-busy="true" on the container while it updates, so assistive technology waits for the final content instead of announcing every intermediate change. Flip it back to false when loading ends.

App.jsxApp.jsx
export default function Results({ loading, children }) {
  return (
    <section aria-busy={loading}>
      {loading ? <Skeleton /> : children}
    </section>
  );
}

While loading is true, screen readers are told the section is still changing. When loading ends, the finished content is exposed once, so the user hears one announcement instead of many. Set aria-busy back to false in the same place you flip loading.

Hide decorative spinners

A spinner is decoration, so hide it from the accessibility tree and pair it with a label.

App.jsxApp.jsx
export default function Spinner({ label }) {
  return (
    <>
      <span className="spinner" aria-hidden="true" />
      <span className="visually-hidden">{label}</span>
    </>
  );
}

The spinning element is aria-hidden so it is not read as content, and the visually hidden span carries the accessible name. Sighted users see the animation while screen reader users hear the label, which should say what is loading rather than just spinner.

Keep skeleton bars out of the tree

Skeleton placeholders are decoration too. Mark the whole skeleton aria-hidden and let the status region carry the message, the same way the layout article recommends keeping the skeleton the same size as the content.

Hiding the bars also stops a screen reader from pausing on each one. See how to design loading states without layout shifts.

Respect reduced motion

The spinner animation should pause or slow when the user prefers reduced motion, so it does not distract or trigger motion sensitivity. Gate the animation behind a media query for prefers-reduced-motion, and keep the status text unchanged so the announcement still happens.

Verify with a screen reader

Open VoiceOver or NVDA and reload the component. You should hear the loading announcement, then the loaded announcement, and nothing from the spinner itself.

If the spinner is read aloud or the change is silent, the aria-hidden or status region is missing. For announcing dynamic content beyond loading, see how to announce dynamic updates to screen readers.

Rune AI

Rune AI

Key Insights

  • Use role=status to announce loading and completion.
  • Set aria-busy=true while content updates, then false.
  • Mark spinners and skeletons aria-hidden.
  • Pair decoration with a visually hidden label.
  • Respect prefers-reduced-motion for animations.
RunePowered by Rune AI

Frequently Asked Questions

Should a spinner be read by screen readers?

No. A spinner is decoration, so mark it aria-hidden and pair it with a visually hidden label or a live region that announces the loading state.

What is the difference between role=status and aria-live?

role=status is a shortcut that implies aria-live=polite and aria-atomic=true, so the whole message is announced politely when it changes.

Conclusion

Accessible loading states announce progress to screen readers and hide decoration from them. Use a status region, aria-busy, and aria-hidden, and respect reduced motion.