How to trap keyboard focus inside a modal dialog?
One of the most popular patterns of an accessible dialog: APG Modal Dialog example outlines the following keyboard behavior:
Tab:
- Moves focus to the next focusable element inside the dialog.
- When focus is on the last dialog's focusable element, it moves focus to the first dialog’s focusable element.
Shift + Tab:
- Moves focus to the previous focusable element inside the dialog.
- When focus is on the first focusable element in the dialog, it moves focus to the last focusable element in the dialog.
Implementation
The APG example provides a link to JavaScript used for the implementation but doesn’t explain in detail how the focus trap works. There are many ways to implement focus trapping, but this explanation focuses on the approach used in the APG example and highlights the key code fragments.
HTML:
<div tabindex="0"></div>
<div role="dialog"...>...</div>
<div tabindex="0"></div>JavaScript:
aria.Dialog.prototype.addListeners = function () {
document.addEventListener('focus', this.trapFocus, true);
};
aria.Dialog.prototype.trapFocus = function (event) {
if (aria.Utils.IgnoreUtilFocusChanges) {
return;
}
var currentDialog = aria.getCurrentDialog();
if (currentDialog.dialogNode.contains(event.target)) {
currentDialog.lastFocus = event.target;
} else {
aria.Utils.focusFirstDescendant(currentDialog.dialogNode);
if (currentDialog.lastFocus == document.activeElement) {
aria.Utils.focusLastDescendant(currentDialog.dialogNode);
}
currentDialog.lastFocus = document.activeElement;
}
};How it works
Two extra focusable nodes are added to the DOM: one before and one after the dialog node. These nodes ensure that focus stays within the dialog by acting as boundaries.
Get Iwona Pękała’s stories in your inbox
Join Medium for free to get updates from this writer.
The trapFocus() function listens for all focus events and handles focus behavior as follows:
- If the focused element is inside the dialog, it records it as the last focused element.
- If the focused element is outside the dialog, focus is immediately moved to the dialog’s first focusable element. This covers the scenario where tabbing moves beyond the bottom dialog’s boundary.
- If the previously recorded last focused element is the dialog’s first focusable element, focus shifts to the dialog’s last focusable element. This handles the case where pressing Shift+Tab moves focus before the top dialog boundary.
Addressing a common focus trapping issue
One common mistake when implementing focus trapping is accidentally including extra nodes in the focus loop. These nodes can disrupt the user’s navigation experience as focus lands on non-meaningful or usually invisible elements. Here’s how to avoid this:
- Ensure extra nodes are excluded from the dialog: Double-check the structure of your HTML to make sure that boundary nodes are not mistakenly included within the dialog.
- Redirect focus away from extra nodes immediately: If the keyboard focus lands on an boundary element, ensure the script instantly redirects it back to a valid element within the dialog.
Testing
When testing your solution, ensure the following:
- Focus never lands on the dialog container itself during tabbing.
- Focus does not move to any non-interactive or invisible elements.
- It is not possible to tab out of the dialog boundaries.
Final note
The described technique addresses keyboard focus management exclusively. Managing screen reader focus is a separate topic that requires additional considerations.









