Imagine you're walking through a building and suddenly find yourself in a room with no lights, no exit signs, and no way to tell where you are or how to get out. You can hear people talking in other rooms, but you can't see anything or find your way back to where you were. This disorienting experience is exactly what happens to keyboard and screen reader users when focus moves to elements that are hidden with aria-hidden="true"
.
ARIA Hidden Focus refers to the problematic situation where elements marked with aria-hidden="true"
can still receive keyboard focus. When this happens, users can tab to these elements, but screen readers won't announce them because they're hidden from assistive technology. This creates a confusing "focus limbo" where users lose track of where they are in the interface.
When focus moves to hidden elements, it creates several critical usability issues:
The fundamental problem is that keyboard focus and screen reader visibility should always be synchronized—when something can receive focus, it should be accessible to all users.
Hidden focus issues represent a breach of trust between the interface and the user. When users press Tab expecting to move to the next visible, interactive element, but instead find themselves in an unmarked, invisible location, it undermines their confidence in the interface and their ability to navigate effectively.
Understanding when and why hidden focus issues arise helps you prevent them in your own interfaces.
When modal dialogs hide background content, focusable elements in the background may remain in the tab order.
<!-- Problematic: Background focusable but hidden -->
<div id="main-content" aria-hidden="true">
<nav>
<a href="/home">Home</a>
<a href="/about">About</a>
</nav>
<main>
<button>Click me</button>
</main>
</div>
<div role="dialog" aria-modal="true">
<h2>Confirmation</h2>
<p>Are you sure you want to delete this item?</p>
<button>Delete</button>
<button>Cancel</button>
</div>
<!-- Better: Remove background elements from tab order -->
<div id="main-content" aria-hidden="true">
<nav>
<a href="/home" tabindex="-1">Home</a>
<a href="/about" tabindex="-1">About</a>
</nav>
<main>
<button tabindex="-1">Click me</button>
</main>
</div>
Mobile navigation menus that are hidden but not properly removed from the tab order.
<!-- Problematic: Hidden menu items still focusable -->
<nav>
<button aria-expanded="false" aria-controls="mobile-menu">Menu</button>
<ul id="mobile-menu" aria-hidden="true" class="collapsed">
<li><a href="/services">Services</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<!-- Better: Proper focus management -->
<nav>
<button aria-expanded="false"
aria-controls="mobile-menu"
onclick="toggleMenu()">Menu</button>
<ul id="mobile-menu" aria-hidden="true" class="collapsed">
<li><a href="/services" tabindex="-1">Services</a></li>
<li><a href="/portfolio" tabindex="-1">Portfolio</a></li>
<li><a href="/contact" tabindex="-1">Contact</a></li>
</ul>
</nav>
Content that's visually hidden off-screen but still contains focusable elements.
<!-- Problematic: Off-screen but focusable -->
<div class="slide-panel" aria-hidden="true" style="position: absolute; left: -9999px;">
<button>Close panel</button>
<form>
<input type="text" placeholder="Search">
<button type="submit">Search</button>
</form>
</div>
<!-- Better: Use display: none or manage tabindex -->
<div class="slide-panel" aria-hidden="true" style="display: none;">
<button>Close panel</button>
<form>
<input type="text" placeholder="Search">
<button type="submit">Search</button>
</form>
</div>
Collapsed accordion sections that hide content but don't manage focus properly.
<!-- Problematic: Collapsed content still focusable -->
<div class="accordion">
<button aria-expanded="false" aria-controls="panel1">Section 1</button>
<div id="panel1" aria-hidden="true" class="panel collapsed">
<p>Panel content...</p>
<a href="/details">More details</a>
<button>Action button</button>
</div>
</div>
<!-- Better: Proper focus management -->
<div class="accordion">
<button aria-expanded="false" aria-controls="panel1">Section 1</button>
<div id="panel1" aria-hidden="true" class="panel collapsed">
<p>Panel content...</p>
<a href="/details" tabindex="-1">More details</a>
<button tabindex="-1">Action button</button>
</div>
</div>
Follow these principles to ensure focus and visibility remain synchronized:
When content should be completely inaccessible, use CSS display: none instead of just aria-hidden.
<!-- Good: Completely hidden content -->
<div class="modal-overlay" style="display: none;">
<div role="dialog">
<h2>Modal Title</h2>
<button>Close</button>
</div>
</div>
<!-- Good: Conditional rendering -->
<div class="dropdown">
<button aria-expanded="false" onclick="toggleDropdown()">Options</button>
<ul id="dropdown-menu" style="display: none;">
<li><a href="/option1">Option 1</a></li>
<li><a href="/option2">Option 2</a></li>
</ul>
</div>
When using aria-hidden, explicitly manage the tabindex of focusable elements within hidden content.
<!-- Dynamic tabindex management -->
<div class="sidebar" id="sidebar" aria-hidden="true">
<nav>
<a href="/dashboard" tabindex="-1">Dashboard</a>
<a href="/settings" tabindex="-1">Settings</a>
<a href="/profile" tabindex="-1">Profile</a>
</nav>
<button tabindex="-1">Close</button>
</div>
<script>
function showSidebar() {
const sidebar = document.getElementById('sidebar');
const focusableElements = sidebar.querySelectorAll('a, button, input, select, textarea');
sidebar.setAttribute('aria-hidden', 'false');
focusableElements.forEach(element => {
element.removeAttribute('tabindex');
});
}
function hideSidebar() {
const sidebar = document.getElementById('sidebar');
const focusableElements = sidebar.querySelectorAll('a, button, input, select, textarea');
sidebar.setAttribute('aria-hidden', 'true');
focusableElements.forEach(element => {
element.setAttribute('tabindex', '-1');
});
}
</script>
The inert attribute makes elements and their descendants non-interactive.
<!-- Modern approach with inert attribute -->
<div id="main-content" inert>
<nav>
<a href="/home">Home</a>
<a href="/about">About</a>
</nav>
<main>
<button>Click me</button>
</main>
</div>
<div role="dialog" aria-modal="true">
<h2>Modal Dialog</h2>
<button>Close</button>
</div>
<script>
function openModal() {
const mainContent = document.getElementById('main-content');
mainContent.setAttribute('inert', '');
mainContent.setAttribute('aria-hidden', 'true');
}
function closeModal() {
const mainContent = document.getElementById('main-content');
mainContent.removeAttribute('inert');
mainContent.removeAttribute('aria-hidden');
}
</script>
For modal dialogs and similar interfaces, implement focus trapping to prevent focus from escaping to hidden content.
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Settings</h2>
<form>
<label for="username">Username:</label>
<input type="text" id="username">
<label for="email">Email:</label>
<input type="email" id="email">
<button type="submit">Save</button>
<button type="button">Cancel</button>
</form>
</div>
<script>
function trapFocus(container) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
container.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
}
});
firstFocusable.focus();
}
</script>
Avoid these frequent issues that create hidden focus problems:
What's happening: Content is visually hidden with CSS positioning or opacity, but remains focusable.
Simple solution: Combine visual hiding with proper focus management:
<!-- Bad: Only visually hidden -->
<div class="hidden-panel" style="position: absolute; left: -9999px;">
<button>Hidden button</button>
</div>
<!-- Good: Properly hidden from all users -->
<div class="hidden-panel" style="display: none;">
<button>Hidden button</button>
</div>
<!-- Or with explicit focus management -->
<div class="hidden-panel" aria-hidden="true">
<button tabindex="-1">Hidden button</button>
</div>
What's happening: Content visibility changes, but focus management isn't updated accordingly.
Simple solution: Always update both visibility and focus states together:
// Bad: Only updating visibility
function togglePanel() {
const panel = document.getElementById('panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
// Good: Comprehensive state management
function togglePanel() {
const panel = document.getElementById('panel');
const focusableElements = panel.querySelectorAll('button, a, input, select, textarea');
const isHidden = panel.style.display === 'none';
if (isHidden) {
panel.style.display = 'block';
panel.setAttribute('aria-hidden', 'false');
focusableElements.forEach(el => el.removeAttribute('tabindex'));
} else {
panel.style.display = 'none';
panel.setAttribute('aria-hidden', 'true');
focusableElements.forEach(el => el.setAttribute('tabindex', '-1'));
}
}
What's happening: Some focusable elements are managed, but others are forgotten.
Simple solution: Use comprehensive selectors to find all focusable elements:
const FOCUSABLE_SELECTOR = [
'button',
'[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]'
].join(', ');
function manageFocusableElements(container, shouldBeFocusable) {
const elements = container.querySelectorAll(FOCUSABLE_SELECTOR);
elements.forEach(element => {
if (shouldBeFocusable) {
element.removeAttribute('tabindex');
} else {
element.setAttribute('tabindex', '-1');
}
});
}
Use these approaches to identify and fix hidden focus problems:
Preventing hidden focus issues delivers several important business benefits:
These benefits compound to create websites that serve all users effectively while supporting business objectives and operational efficiency.
Different types of interfaces require specific approaches to preventing hidden focus issues:
In each case, the principle is the same: focus should only move to elements that are accessible and meaningful to all users.
Focus serves as a navigation compass for keyboard and screen reader users, showing them exactly where they are in your interface and where they can go next. When focus moves to hidden elements, it's like a compass that suddenly starts pointing in random directions—users lose their bearings and their confidence in the interface.
The principle behind preventing hidden focus issues is simple but crucial: focus and accessibility should always be synchronized. If something can receive focus, it should be accessible to all users. If something is hidden from assistive technologies, it should also be hidden from keyboard navigation.
What makes hidden focus issues particularly problematic is that they often occur in interfaces that are otherwise well-designed and accessible. A single overlooked focusable element in a hidden container can break the entire navigation experience for keyboard users, turning a usable interface into an unusable one.
By implementing proper focus management—whether through careful tabindex management, the inert attribute, or comprehensive visibility state management—you ensure that your interface provides a reliable, predictable navigation experience for all users. This reliability is the foundation of truly accessible web experiences.
Greadme's easy-to-use tools can help you identify elements that can receive focus while hidden, and provide clear guidance on fixing these navigation barriers—even if you're not technically minded.
Check Your Website's Focus Management Today