Focus Trapping
Focus Trapping
Keep keyboard focus within a component (modals, drawers).
Focus Trap Hook
// hooks/useFocusTrap.js
function useFocusTrap(isActive) {
const containerRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (!isActive) return;
// Remember previous focus
previousFocusRef.current = document.activeElement;
// Get focusable elements
const container = containerRef.current;
const focusable = container.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusable[0];
const lastElement = focusable[focusable.length - 1];
// Focus first element
firstElement?.focus();
const handleKeyDown = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift+Tab: if at first, go to last
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else {
// Tab: if at last, go to first
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
};
container.addEventListener('keydown', handleKeyDown);
return () => {
container.removeEventListener('keydown', handleKeyDown);
// Restore previous focus
previousFocusRef.current?.focus();
};
}, [isActive]);
return containerRef;
}
Modal Component
function Modal({ isOpen, onClose, title, children }) {
const containerRef = useFocusTrap(isOpen);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div
ref={containerRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(e) => e.stopPropagation()}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close modal">
×
</button>
</div>
</div>
);
}
Accessible Dialog
function ConfirmDialog({ isOpen, onConfirm, onCancel, message }) {
const confirmRef = useRef(null);
useEffect(() => {
if (isOpen) {
confirmRef.current?.focus();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onClose={onCancel} title="Confirm Action">
<p>{message}</p>
<div className="dialog-actions">
<button onClick={onCancel}>Cancel</button>
<button ref={confirmRef} onClick={onConfirm}>
Confirm
</button>
</div>
</Modal>
);
}
Modal Focus
Modal Focus
Handle focus properly in modal dialogs.
Modal Focus Management
function Dialog({ isOpen, onClose, children }) {
const dialogRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save current focus
previousFocusRef.current = document.activeElement;
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Add aria-hidden to background
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.setAttribute('aria-hidden', 'true');
mainContent.setAttribute('inert', '');
}
}
return () => {
document.body.style.overflow = '';
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.removeAttribute('aria-hidden');
mainContent.removeAttribute('inert');
}
previousFocusRef.current?.focus();
};
}, [isOpen]);
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
if (!isOpen) return null;
return (
<div className="dialog-overlay" role="dialog" aria-modal="true">
<div
ref={dialogRef}
className="dialog-content"
onKeyDown={handleKeyDown}
>
{children}
</div>
</div>
);
}
Focus Restoration
// Store focus reference
const focusHistory = [];
function pushFocus(element) {
focusHistory.push(document.activeElement);
element?.focus();
}
function popFocus() {
const previous = focusHistory.pop();
previous?.focus();
}
// Usage
function App() {
const [modalOpen, setModalOpen] = useState(false);
const openModal = () => {
pushFocus(document.getElementById('open-button'));
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
popFocus();
};
return (
<div>
<button id="open-button" onClick={openModal}>
Open Modal
</button>
<Modal isOpen={modalOpen} onClose={closeModal} />
</div>
);
}
Route Change Focus
Route Change Focus
Manage focus when navigating between pages.
Announce Route Changes
// Live region for announcements
function RouteAnnouncer() {
const [announcement, setAnnouncement] = useState('');
const location = useLocation();
useEffect(() => {
// Announce new page
setAnnouncement('');
const timer = setTimeout(() => {
const title = document.title || 'New page';
setAnnouncement(`Navigated to ${title}`);
}, 100);
return () => clearTimeout(timer);
}, [location.pathname]);
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
);
}
Focus on Navigation
// Focus main content on route change
function PageWrapper({ children }) {
const mainRef = useRef(null);
const location = useLocation();
useEffect(() => {
// Focus main content on route change
mainRef.current?.focus();
// Scroll to top
window.scrollTo(0, 0);
}, [location.pathname]);
return (
<main
ref={mainRef}
tabIndex={-1}
style={{ outline: 'none' }}
>
{children}
</main>
);
}
Skip Navigation
function Layout({ children }) {
return (
<div>
{/* Skip link */}
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<header>
<nav aria-label="Main">{/* ... */}</nav>
</header>
<main id="main-content" tabIndex={-1}>
{children}
</main>
</div>
);
}
// CSS for skip link
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 50%;
transform: translateX(-50%);
z-index: 9999;
background: white;
padding: 8px 16px;
border: 2px solid black;
}
Focus Management Checklist
- Modal traps focus
- Focus returns to trigger on close
- Route changes announce to screen readers
- Skip links provided
- Focus visible on interactive elements
- No keyboard traps
Practice Problems
Create a reusable React component implementing Focus Management. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Focus Management using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Focus Management for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is focus trapping?
2. Where should focus go when a modal closes?
3. What is the primary purpose of Focus Management?
4. What is a common mistake when implementing Focus Management?
Flashcards
Question
What is focus trapping?
Click to reveal answer
Answer
Keeping keyboard focus within a component (like a modal) until it's dismissed.
Question
Why restore focus on modal close?
Click to reveal answer
Answer
To maintain context and good UX for keyboard users.
Question
What is route change focus management?
Click to reveal answer
Answer
Focusing the main content and announcing the new page to screen readers.
Question
What is a skip link?
Click to reveal answer
Answer
A link that lets keyboard users skip navigation and go directly to main content.
Question
What is Focus Management?
Click to reveal answer
Answer
Focus Management is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Trap focus in modals and dialogs
- 2.Restore focus when components close
- 3.Announce route changes to screen readers
- 4.Use skip links for keyboard navigation
- 5.Manage focus for dynamic content
Interview Tips
- •Explain how focus trapping works
- •Discuss focus management in SPAs
- •Know how to implement accessible modals
Cheat Sheet
Focus Management Cheat Sheet
Focus Trap
- Save previous focus
- Find focusable elements
- Handle Tab/Shift+Tab
- Restore focus on unmount
Modal Focus
- Trap focus inside
- Prevent body scroll
- aria-hidden background
- Restore focus on close
Route Changes
- Announce to screen readers
- Focus main content
- Scroll to top
- Skip link support