How to Implement Focus Management with FocusTrap for Accessibility in Material-UI
Material-UI's TrapFocus component automatically confines keyboard navigation within modal containers, ensuring compliance with WAI-ARIA accessibility standards by cycling focus through interactive elements and restoring focus when the modal closes.
Implementing focus management with FocusTrap for accessibility in Material-UI is essential for creating inclusive modal experiences that support keyboard and screen reader users. The mui/material-ui repository provides a robust focus-trapping implementation through the TrapFocus component, located in packages/material-ui/src/Modal/TrapFocus.tsx, which handles focus capture, tab cycling, and restoration automatically.
How FocusTrap Works in Material-UI
The focus-trap architecture in Material-UI consists of three coordinated systems that manage focus state across modal layers.
TrapFocus Component Architecture
The TrapFocus component in packages/material-ui/src/Modal/TrapFocus.tsx serves as the core implementation. When the open prop is true, the component:
- Saves the previously focused element (the element that had focus before the modal opened) to restore it later.
- Moves focus to the first focusable element within the modal, or to a specific element specified by the
autoFocusprop. - Listens for
TabandShift+Tabkey events and programmatically cycles focus back to the first or last focusable element when the user attempts to tab beyond the modal boundaries. - Restores the previously saved focus when the modal unmounts or the
openprop becomes false.
ModalManager Stack Coordination
The ModalManager class in packages/material-ui/src/Modal/ModalManager.ts maintains a stack of open modals to handle nested modal scenarios. This ensures that when multiple modals are open simultaneously, only the top-most modal receives focus-trap enforcement, preventing focus conflicts between layered dialogs.
Implementing FocusTrap in Dialogs and Modals
Material-UI's high-level components automatically compose TrapFocus, making standard implementation straightforward.
Basic Dialog Implementation
The Dialog component in packages/material-ui/src/Dialog/Dialog.tsx inherits focus-trap behavior from Modal. When you render a Dialog, focus management works automatically:
import * as React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
export default function AccessibleDialog() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button variant="outlined" onClick={() => setOpen(true)}>
Open dialog
</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<DialogTitle id="dialog-title">
Accessible Dialog
</DialogTitle>
<DialogContent id="dialog-description">
This dialog automatically traps focus inside while open,
cycling through interactive elements and restoring
focus to the trigger button on close.
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</>
);
}
The Dialog automatically applies role="dialog" and composes TrapFocus internally, ensuring keyboard users cannot tab out of the modal while it is open.
Disabling FocusTrap for Non-Modal Components
Some components like Popover in packages/material-ui/src/Popover/Popover.tsx disable the focus trap by default using the disableEnforceFocus prop, allowing users to interact with the underlying page. You can apply this pattern when building non-modal overlays:
import * as React from 'react';
import Popover from '@mui/material/Popover';
import Button from '@mui/material/Button';
export default function NonModalPopover() {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const open = Boolean(anchorEl);
return (
<>
<Button
aria-describedby={open ? 'non-modal-popover' : undefined}
variant="contained"
onClick={handleClick}
>
Show popover
</Button>
<Popover
id="non-modal-popover"
open={open}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
disableEnforceFocus // Focus is NOT trapped
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
>
<div style={{ padding: 16 }}>
This popover does not trap focus; users can continue
navigating the underlying page while it remains visible.
</div>
</Popover>
</>
);
}
Using TrapFocus Directly in Custom Components
For custom modal implementations that do not use Dialog or Modal, import TrapFocus directly from the Modal module:
import * as React from 'react';
import { TrapFocus } from '@mui/material/Modal';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
export default function CustomFocusTrap() {
const [open, setOpen] = React.useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>
Open custom modal
</Button>
{open && (
<TrapFocus
disableAutoFocus={false}
disableEnforceFocus={false}
isEnabled={() => true}
open={open}
>
<Box
sx={{
position: 'fixed',
top: '50%',
left: '50%',
width: 300,
p: 2,
bgcolor: 'background.paper',
boxShadow: 24,
transform: 'translate(-50%, -50%)',
}}
role="dialog"
aria-modal="true"
aria-labelledby="custom-modal-title"
tabIndex={-1}
>
<h2 id="custom-modal-title">Custom Focus Trap</h2>
<p>Focus is confined inside this container.</p>
<Button onClick={() => setOpen(false)}>Close</Button>
</Box>
</TrapFocus>
)}
</>
);
}
When using TrapFocus directly, ensure you set role="dialog" and aria-modal="true" on the container to communicate the modal context to assistive technologies.
Key Source Files in Material-UI
Understanding the source architecture helps when debugging focus behavior or extending functionality:
| File | Purpose |
|---|---|
packages/material-ui/src/Modal/TrapFocus.tsx |
Core focus-trap implementation handling focus capture, tab cycling, and restoration. |
packages/material-ui/src/Modal/ModalManager.ts |
Manages the stack of open modals to coordinate focus behavior across nested layers. |
packages/material-ui/src/Modal/Modal.tsx |
Generic modal component that composes TrapFocus and provides disableEnforceFocus prop. |
packages/material-ui/src/Dialog/Dialog.tsx |
High-level dialog component built on Modal with automatic ARIA roles and focus trapping. |
packages/material-ui/src/Popover/Popover.tsx |
Example component that sets disableEnforceFocus by default for non-modal behavior. |
Summary
- TrapFocus in
packages/material-ui/src/Modal/TrapFocus.tsxautomatically confines keyboard navigation within modal containers, saving and restoring focus to maintain accessibility. - Dialog and Modal components include focus trapping by default, requiring no additional configuration for standard use cases.
- Use the
disableEnforceFocusprop when building non-modal overlays like tooltips or popovers where users should interact with the underlying page. - Import TrapFocus directly from
@mui/material/Modalfor custom modal implementations that require fine-grained focus control. - Always include
role="dialog",aria-modal="true", and labeling attributes to ensure screen readers correctly announce modal context.
Frequently Asked Questions
What is the TrapFocus component in Material-UI?
The TrapFocus component is a low-level utility exported from @mui/material/Modal that manages focus within a contained DOM subtree. According to the source code in packages/material-ui/src/Modal/TrapFocus.tsx, it captures focus when activated, cycles Tab navigation through focusable elements, and restores focus to the previously active element when deactivated, ensuring compliance with WCAG guidelines for modal dialogs.
How do I disable focus trapping in a Material-UI Dialog?
To disable focus trapping, pass the disableEnforceFocus prop to the Dialog or underlying Modal component. As implemented in packages/material-ui/src/Modal/Modal.tsx, this boolean prop prevents the TrapFocus mechanism from intercepting Tab key events, allowing users to navigate outside the modal. This is useful for non-modal popovers or when implementing custom focus management.
Why does focus trap automatically in Material-UI Dialogs but not in Popovers?
Material-UI's Dialog component in packages/material-ui/src/Dialog/Dialog.tsx is designed as a modal window that blocks interaction with the rest of the page, so it enables focus trapping by default through the underlying Modal component. Conversely, Popover in packages/material-ui/src/Popover/Popover.tsx sets disableEnforceFocus to true by default because popovers are typically non-modal overlays that allow users to continue interacting with the underlying page content while the popover remains visible.
Can I use TrapFocus outside of Dialog and Modal components?
Yes, you can import TrapFocus directly from @mui/material/Modal to create custom accessible components. As shown in packages/material-ui/src/Modal/TrapFocus.tsx, the component accepts props like open, disableAutoFocus, and disableEnforceFocus, allowing you to wrap any JSX element to create a focus-managed region. When using it directly, remember to add appropriate ARIA attributes like role="dialog" and aria-modal="true" to the container element to ensure screen readers announce the modal context correctly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →