Portals with useRef
Portals with useRef and Focus Management
Good modal UX requires focus trapping: when a modal opens, keyboard focus must
stay inside it. When it closes, focus should return to the trigger element.
Focus Trap Pattern
import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';
function AccessibleModal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save who had focus before modal opened
previousFocusRef.current = document.activeElement;
// Move focus into the modal
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable?.length) focusable[0].focus();
} else {
// Return focus to the trigger element
previousFocusRef.current?.focus();
}
}, [isOpen]);
// Trap Tab key inside modal
function handleKeyDown(e) {
if (e.key !== 'Tab') return;
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
if (!isOpen) return null;
return createPortal(
<div
ref={modalRef}
role="dialog"
aria-modal="true"
onKeyDown={handleKeyDown}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}
>
<div style={{ background: '#fff', padding: '2rem', borderRadius: '8px' }}>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.getElementById('modal-root')
);
}
export default AccessibleModal;
