Imagine walking into a library where all the books are invisible to you, even though other people can see and read them perfectly. You know the information exists, but you have no way to access it. This is exactly what happens when developers use aria-hidden="true"
on large sections of a webpage or the entire body element—they accidentally make content completely invisible to screen readers and other assistive technologies.
ARIA Hidden Body refers to the problematic practice of applying aria-hidden="true"
to the body element, main content areas, or other large sections of a webpage. While aria-hidden
is useful for hiding decorative or redundant elements, when applied too broadly, it can make entire websites inaccessible to users who rely on assistive technologies.
Using aria-hidden on substantial content creates severe accessibility barriers with far-reaching consequences:
The fundamental problem is that aria-hidden is designed to hide small, decorative, or redundant elements—not substantial content that users need to access and understand.
One of the most troubling aspects of inappropriate aria-hidden usage is that it often happens when developers are trying to improve accessibility. They may hide content thinking it will reduce clutter for screen reader users, but instead they create barriers where none should exist. Good intentions can inadvertently create the very problems they're meant to solve.
Understanding when aria-hidden is commonly misused helps you avoid these pitfalls:
Developers sometimes hide content with aria-hidden while it's loading, forgetting to remove the attribute when content becomes available.
<!-- Problematic: Content remains hidden after loading -->
<main id="content" aria-hidden="true">
<h1>Welcome to Our Site</h1>
<p>This content loaded but is still hidden from screen readers</p>
</main>
<!-- Better: Use loading states that don't hide content -->
<main id="content">
<div aria-live="polite" id="loading-status">
Loading content...
</div>
<div id="actual-content" style="display: none;">
<h1>Welcome to Our Site</h1>
<p>This content will be revealed when ready</p>
</div>
</main>
<script>
// Good: Remove loading state, show content
function showContent() {
document.getElementById('loading-status').textContent = '';
document.getElementById('actual-content').style.display = 'block';
// No aria-hidden needed
}
</script>
When implementing modal dialogs, developers may hide the entire page background, but this can be too aggressive.
<!-- Too aggressive: Hides everything -->
<body aria-hidden="true">
<header>Site header</header>
<nav>Navigation</nav>
<main>Main content</main>
<footer>Footer</footer>
</body>
<!-- Better: Target specific areas -->
<body>
<div id="page-content" aria-hidden="true">
<header>Site header</header>
<nav>Navigation</nav>
<main>Main content</main>
<footer>Footer</footer>
</div>
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Important Notice</h2>
<p>Dialog content here</p>
<button onclick="closeDialog()">Close</button>
</div>
</body>
<script>
function openDialog() {
document.getElementById('page-content').setAttribute('aria-hidden', 'true');
// Focus management for dialog
}
function closeDialog() {
document.getElementById('page-content').removeAttribute('aria-hidden');
// Return focus to trigger
}
</script>
In SPAs, developers might hide content during route transitions but fail to properly reveal the new content.
// Problematic: Hiding content without proper reveal
function navigateToPage(pageId) {
// Hide all pages
document.querySelectorAll('.page').forEach(page => {
page.setAttribute('aria-hidden', 'true');
});
// Show target page - but aria-hidden might remain
document.getElementById(pageId).style.display = 'block';
// Missing: Remove aria-hidden from target page
}
// Better: Proper content management
function navigateToPage(pageId) {
// Hide all pages properly
document.querySelectorAll('.page').forEach(page => {
page.style.display = 'none';
page.setAttribute('aria-hidden', 'true');
});
// Show and reveal target page
const targetPage = document.getElementById(pageId);
targetPage.style.display = 'block';
targetPage.removeAttribute('aria-hidden');
// Focus management
const heading = targetPage.querySelector('h1');
if (heading) {
heading.tabIndex = -1;
heading.focus();
}
}
Some developers hide content thinking it will make the screen reader experience "cleaner," but this removes important context.
<!-- Problematic: Hiding useful content -->
<article>
<header aria-hidden="true">
<time datetime="2025-05-21">May 21, 2025</time>
<span>By John Smith</span>
</header>
<h1>Article Title</h1>
<p>Article content...</p>
<aside aria-hidden="true">
<h2>Related Articles</h2>
<ul>
<li><a href="/article1">Related Article 1</a></li>
<li><a href="/article2">Related Article 2</a></li>
</ul>
</aside>
</article>
<!-- Better: Let users access all relevant information -->
<article>
<header>
<time datetime="2025-05-21">May 21, 2025</time>
<span>By John Smith</span>
</header>
<h1>Article Title</h1>
<p>Article content...</p>
<aside>
<h2>Related Articles</h2>
<ul>
<li><a href="/article1">Related Article 1</a></li>
<li><a href="/article2">Related Article 2</a></li>
</ul>
</aside>
</article>
Understanding the correct use cases for aria-hidden helps you apply it appropriately:
Hide purely decorative content that doesn't convey meaningful information.
<!-- Good: Hiding decorative icons -->
<button>
<span aria-hidden="true">🔍</span>
Search
</button>
<div class="card">
<div class="decorative-border" aria-hidden="true"></div>
<h2>Card Title</h2>
<p>Card content</p>
</div>
<!-- Good: Hiding redundant visual elements -->
<a href="/download">
Download Report
<span aria-hidden="true">(PDF, 2MB)</span>
</a>
<!-- The file info is useful visually but redundant for screen readers
since the filename usually indicates format -->
Hide content that duplicates information already available to screen readers.
<!-- Good: Hiding visual-only duplicates -->
<div class="notification">
<h2>Success</h2>
<p>Your changes have been saved.</p>
<div class="visual-checkmark" aria-hidden="true">✓</div>
</div>
<!-- The checkmark is redundant since "Success" already conveys the meaning -->
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget A</td>
<td>$19.99</td>
<td>
<button aria-label="Edit Widget A">
<span aria-hidden="true">✏️</span>
</button>
</td>
</tr>
</tbody>
</table>
<!-- Icon is hidden because aria-label provides better context -->
When implementing modal dialogs, you may hide background content, but be surgical about it.
<!-- Good: Careful modal implementation -->
<div id="app">
<div id="main-content">
<header>
<h1>Site Title</h1>
<nav>Navigation menu</nav>
</header>
<main>
<h1>Page Content</h1>
<button onclick="openModal()">Open Settings</button>
</main>
</div>
<div id="modal-container"></div>
</div>
<script>
function openModal() {
// Create modal
const modal = document.createElement('div');
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'modal-title');
modal.innerHTML = `
<h2 id="modal-title">Settings</h2>
<p>Modal content...</p>
<button onclick="closeModal()">Close</button>
`;
// Hide background content
document.getElementById('main-content').setAttribute('aria-hidden', 'true');
// Add modal and focus
document.getElementById('modal-container').appendChild(modal);
modal.querySelector('h2').focus();
}
function closeModal() {
// Remove modal
document.getElementById('modal-container').innerHTML = '';
// Restore background content
document.getElementById('main-content').removeAttribute('aria-hidden');
// Return focus
document.querySelector('button[onclick="openModal()"]').focus();
}
</script>
Instead of using aria-hidden on large content sections, consider these better approaches:
Use loading indicators and progressive content revelation instead of hiding content.
<!-- Better approach for loading content -->
<main>
<div id="loading-indicator" aria-live="polite">
Loading page content...
</div>
<div id="content" class="loading">
<h1>Page Title</h1>
<p>This content will be revealed when ready.</p>
</div>
</main>
<style>
.loading {
opacity: 0.5;
pointer-events: none;
}
.loaded {
opacity: 1;
pointer-events: auto;
}
</style>
<script>
function contentLoaded() {
const indicator = document.getElementById('loading-indicator');
const content = document.getElementById('content');
indicator.textContent = 'Content loaded';
content.classList.remove('loading');
content.classList.add('loaded');
// Clear loading message after brief delay
setTimeout(() => {
indicator.textContent = '';
}, 1000);
}
</script>
Use proper HTML structure and CSS for visual management instead of hiding content.
<!-- Good: Semantic structure with CSS management -->
<div class="page-container">
<aside class="sidebar">
<nav>
<h2>Section Navigation</h2>
<ul>
<li><a href="#section1">Section 1</a></li>
<li><a href="#section2">Section 2</a></li>
</ul>
</nav>
</aside>
<main class="main-content">
<section id="section1">
<h1>Section 1 Title</h1>
<p>Content...</p>
</section>
<section id="section2" class="initially-hidden">
<h1>Section 2 Title</h1>
<p>Content...</p>
</section>
</main>
</div>
<style>
.initially-hidden {
display: none;
}
.visible {
display: block;
}
/* Responsive hiding without accessibility impact */
@media (max-width: 768px) {
.sidebar {
display: none;
}
.mobile-menu-active .sidebar {
display: block;
}
}
</style>
<script>
function showSection(sectionId) {
// Hide all sections
document.querySelectorAll('section').forEach(section => {
section.classList.add('initially-hidden');
});
// Show target section
const target = document.getElementById(sectionId);
target.classList.remove('initially-hidden');
target.classList.add('visible');
// Focus management
target.querySelector('h1').focus();
}
</script>
Reveal content progressively based on user actions instead of hiding large sections.
<!-- Good: Progressive disclosure pattern -->
<div class="content-sections">
<section class="summary">
<h2>Quick Overview</h2>
<p>Brief summary of key points...</p>
<button onclick="showDetails()" aria-expanded="false" aria-controls="detailed-content">
Show Detailed Information
</button>
</section>
<section id="detailed-content" class="details" hidden>
<h2>Detailed Information</h2>
<p>Comprehensive details...</p>
<button onclick="hideDetails()">Show Less</button>
</section>
</div>
<script>
function showDetails() {
const button = document.querySelector('[aria-controls="detailed-content"]');
const details = document.getElementById('detailed-content');
button.setAttribute('aria-expanded', 'true');
button.textContent = 'Show Less Information';
details.hidden = false;
// Focus the detailed content
details.querySelector('h2').focus();
}
function hideDetails() {
const button = document.querySelector('[aria-controls="detailed-content"]');
const details = document.getElementById('detailed-content');
button.setAttribute('aria-expanded', 'false');
button.textContent = 'Show Detailed Information';
details.hidden = true;
// Return focus to trigger
button.focus();
}
</script>
Use these approaches to find and resolve inappropriate aria-hidden usage:
Use accessibility testing tools that can identify problematic aria-hidden usage.
// Example: Testing for inappropriate aria-hidden
describe('aria-hidden usage', () => {
test('should not hide large content areas', () => {
render(<App />);
// Check that main content areas are not hidden
const main = screen.getByRole('main');
expect(main).not.toHaveAttribute('aria-hidden', 'true');
const navigation = screen.getByRole('navigation');
expect(navigation).not.toHaveAttribute('aria-hidden', 'true');
// Check that body is not hidden
expect(document.body).not.toHaveAttribute('aria-hidden', 'true');
});
test('should only hide decorative elements', () => {
render(<IconButton />);
// Decorative icons should be hidden
const decorativeIcon = screen.getByTestId('decorative-icon');
expect(decorativeIcon).toHaveAttribute('aria-hidden', 'true');
// But the button text should not be hidden
const buttonText = screen.getByText('Save');
expect(buttonText).not.toHaveAttribute('aria-hidden', 'true');
});
});
// Automated check for large hidden areas
function checkForProblematicAriaHidden() {
const hiddenElements = document.querySelectorAll('[aria-hidden="true"]');
hiddenElements.forEach(element => {
// Check if hidden element is large (contains many child elements)
const childCount = element.querySelectorAll('*').length;
if (childCount > 10) {
console.warn('Large content area is hidden:', element);
}
// Check if hidden element contains important semantic elements
const semanticElements = element.querySelectorAll('main, nav, header, footer, article, section');
if (semanticElements.length > 0) {
console.warn('Important semantic content is hidden:', element);
}
});
}
Test with actual screen readers to identify when content is inappropriately hidden.
Implement code review practices that catch inappropriate aria-hidden usage.
// ESLint rule to catch problematic aria-hidden usage
{
"rules": {
"jsx-a11y/aria-hidden": ["error", {
"checkOtherProps": true
}],
"custom/no-large-aria-hidden": "error"
}
}
// Custom ESLint rule example
module.exports = {
"no-large-aria-hidden": {
create: function(context) {
return {
JSXAttribute: function(node) {
if (node.name.name === 'aria-hidden' &&
node.value.value === true) {
// Check if applied to semantic elements
const elementName = node.parent.name.name;
const problematicElements = ['main', 'nav', 'header', 'footer', 'article', 'section'];
if (problematicElements.includes(elementName)) {
context.report({
node,
message: 'aria-hidden should not be used on semantic elements'
});
}
}
}
};
}
}
};
Regular testing ensures that your aria-hidden usage doesn't create accessibility barriers:
The most important test is whether users can complete all essential tasks using only assistive technology—if content is inappropriately hidden, this will be impossible.
Ensuring content remains accessible delivers significant business benefits:
These benefits compound to create websites that serve all users effectively while supporting business objectives and legal compliance requirements.
Different types of websites require tailored approaches to content visibility management:
In each case, the principle is the same: use aria-hidden sparingly and only for truly decorative or redundant content, never for substantial information or functionality.
The misuse of aria-hidden on large content areas represents one of the most serious accessibility barriers you can accidentally create. Unlike many accessibility issues that create friction or confusion, inappropriate content hiding can make websites completely unusable for people who rely on assistive technologies.
What makes this issue particularly problematic is that it often happens with good intentions. Developers trying to "clean up" the screen reader experience or manage complex interfaces can inadvertently hide the very content users need to access. The road to inaccessibility is often paved with good intentions.
The principle behind proper aria-hidden usage reflects a broader accessibility truth: inclusion means giving all users access to the same information and functionality, not deciding what they should or shouldn't experience. When we hide content from assistive technology users, we make decisions about their needs that they should be making themselves.
As web experiences become more complex and interactive, the temptation to manage screen reader experiences through content hiding may increase. Resist this temptation. Instead, focus on creating well-structured, semantic content that works for everyone, using aria-hidden only for the specific purpose it was designed for: hiding truly decorative or redundant elements.
Greadme's easy-to-use tools can help you identify inappropriate aria-hidden usage on your website and provide clear guidance on making all your content accessible—even if you're not technically minded.
Check Your Website's Content Visibility Today