Keyboard users often encounter issues when interacting with popins, modals, or tooltips. During an accessibility audit with a partner, we noticed that focus can easily escape the popin, leading to a confusing experience.
This approach ensures keyboard users can stay within the popin and interact predictably, improving accessibility and user experience (UX) for all users.
The problem
Popins generally include several interactive elements: a close button, overlay, and main content such as forms or call-to-actions (CTAs). Without proper focus management, users can tab out of the popin into the underlying page, breaking accessibility rules and making interactions unpredictable.
Implementation
Key rules for accessible popins:
Initial focus on the close button
When a popin opens, we save the currently focused element and programmatically set focus to the close button using .focus(). This ensures that keyboard users start interacting with the popin immediately and allows us to restore focus later when the popin closes.
Trap focus inside the popin
Tabulation should only traverse elements inside the popin. The focus remains trapped only while the popin is open. Reaching the last element and pressing ⇥ Tab returns to the first, and ⇧ Shift + ⇥ Tab from the first moves to the last.
When the popin is closed, focus is restored to the element that triggered its opening.
If the popin opens automatically without user interaction, do not move focus; instead, ensure the popin is keyboard-accessible and optionally notify the user via an ARIA live region.
Visible focus styling
Using :focus-visible ensures keyboard users can see which element is active. This pseudo-class only applies when interacting via keyboard, avoiding unnecessary outlines for mouse users.
💡 A good practice is to allow the popin to be closed by pressing the Escape or clicking on the overlay.
Example
Here's a minimal example of a popin with HTML, CSS, and JS implementing the focus trap pattern:
HTML
<div role="dialog" aria-modal="true" aria-labelledby="newsletter-title" class="popin">
<button type="button" class="popin-close" aria-label="Close newsletter popin">×</button>
<h2 id="newsletter-title">Subscribe to our newsletter</h2>
<form>
<input type="email" placeholder="Your email" required />
<button type="submit" class="form-submit">Subscribe</button>
</form>
</div>
CSS
.popin *:focus-visible {
outline: 2px solid #007acc;
outline-offset: 2px;
}
JavaScript
class FocusTrap {
constructor({ container, firstElement, lastElement }) {
this.container = container;
this.firstElement = firstElement;
this.lastElement = lastElement;
this.previousActiveElement = null;
this.focusableSelector =
'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
this.handleKeydown = this.handleKeydown.bind(this);
}
init() {
this.previousActiveElement = document.activeElement;
this.focusableContent = [...this.container.querySelectorAll(this.focusableSelector)];
this.firstElement.focus();
document.addEventListener('keydown', this.handleKeydown);
}
handleKeydown(e) {
if (e.key !== 'Tab') return;
// Recalculate focusable elements
this.focusableContent = [...this.container.querySelectorAll(this.focusableSelector)];
const last = this.lastElement ?? this.focusableContent.at(-1);
if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
this.firstElement.focus();
} else if (e.shiftKey && document.activeElement === this.firstElement) {
e.preventDefault();
last.focus();
}
}
destroy() {
document.removeEventListener('keydown', this.handleKeydown);
this.previousActiveElement?.focus();
}
}
Key points
- Always set initial focus to the most relevant interactive element (usually the close button)
- Trap focus inside popins to prevent keyboard users from leaving the overlay unintentionally
- Use
:focus-visibleto ensure the active element is clearly visible for keyboard navigation - Maintain correct DOM order and define
tabindexfor logical navigation - Include proper ARIA roles and attributes (
role="dialog",aria-modal="true",aria-labelledby, button types) - Restore focus to the triggering element after closing the popin
- Handle automatically opened popins appropriately (keyboard-accessible, ARIA live notification)
- Modularize the focus trap pattern for reuse across multiple popins
Conclusion
Focus management for popins is a small but crucial detail in accessibility. Implementing a focus trap provides a predictable experience for keyboard users and can prevent common accessibility pitfalls.
Thinking about focus management early in your modal design ensures a smoother and more consistent experience for all users, and the pattern is reusable across multiple sites or projects.
Resources
- WCAG 2.1 - Keyboard accessibility (2.1.1)
- WCAG 2.1 - Focus order (2.4.3)
- Dialog (Modal) pattern
focus-trapGitHub repository
Top comments (0)