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.
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.
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.
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
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.
Frequently Asked Questions
Should a spinner be read by screen readers?
What is the difference between role=status and aria-live?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.