data-focus-trap
Constrain keyboard focus within a container. Tab/Shift+Tab cycle inside the boundary, focus auto-moves in on activation, and restores on deactivation.
Overview
The data-focus-trap attribute constrains keyboard focus within a container. Tab and Shift+Tab cycle through focusable children without escaping. When the trap activates, focus moves into the container. When the trap deactivates, focus returns to the previously focused element.
Designed for non-modal panels, inline editing regions, and dynamically inserted content where the native <dialog> focus trap is not available.
<section data-focus-trap> <input type="text" autofocus> <button>Save</button> <button>Cancel</button></section>
How It Works
- Activation: When a
[data-focus-trap]element enters the DOM (or the attribute is added), focus moves to[autofocus]inside the container, or the first focusable child. The previously focused element is saved. - Trapping: Pressing Tab on the last focusable child wraps to the first. Pressing Shift+Tab on the first wraps to the last.
- Deactivation: When the element is removed from the DOM (or the attribute is removed), focus restores to the saved element.
Values
| Value | Behavior |
|---|---|
"" (empty/default) |
Trap focus and auto-focus [autofocus] or first focusable child on activation. |
no-autofocus |
Trap focus but do not move focus on activation. Useful for toolbars or panels that should trap once the user tabs in. |
Dynamic Content
The trap watches the DOM with a MutationObserver. Adding a data-focus-trap element dynamically activates it immediately. Removing the element restores focus automatically.
<button id="open-btn">Edit</button><div id="panel-target"></div> <script> document.getElementById('open-btn').addEventListener('click', () => { const panel = document.createElement('section'); panel.setAttribute('data-focus-trap', ''); panel.innerHTML = ` <input type="text" autofocus> <button id="close-btn">Close</button> `; document.getElementById('panel-target').appendChild(panel); panel.querySelector('#close-btn').addEventListener('click', () => { panel.remove(); // focus auto-restores to #open-btn }); });</script>
Toggling via Attribute
You can also activate and deactivate a trap by adding or removing the attribute on an existing element:
JavaScript API
The module exports functions for programmatic control:
import { activateTrap, deactivateTrap } from './utils/focus-trap-init.js'; // Activate on a specific elementactivateTrap(myPanel); // Deactivate and restore focusdeactivateTrap(myPanel);
Keyboard
| Key | Action |
|---|---|
| Tab | Move to next focusable child. Wraps from last to first. |
| Shift + Tab | Move to previous focusable child. Wraps from first to last. |
Accessibility
- Focus trapping prevents keyboard users from reaching content behind an active panel.
- Focus restoration ensures users return to their original context when the panel closes.
- The
[autofocus]integration provides immediate keyboard access to the primary input. - Use
aria-labelor a heading inside the trap to give screen reader context about the trapped region.
When to Use
- Non-modal slide panels: Settings or filter panels opened with
show()(notshowModal()). - Inline editing: Forms that appear in-page and should capture keyboard focus.
- Toolbars: Rich text or formatting toolbars with
no-autofocus. - Dynamic content: Panels loaded via fetch or template insertion.
When Not to Use
- Modal dialogs: Use
<dialog>withshowModal()instead — it traps focus natively. - Popovers: Popover content typically uses light-dismiss, not focus trapping.
- Page sections: Don’t trap focus in regular content regions — users expect free navigation.