🇮🇳
🇮🇳
Republic Day Special Offer!Get 20% OFF on all courses
Enroll Now
P
Prakalpana
📚Learn
Code Your Future
System Design⏱️ 15 min read📅 Dec 25

UI System Design: Modal/Dialog System with Portal

KS
Kavya SharmaFrontend Lead at Airbnb
📑 Contents (6 sections)

📌Problem Statement

Design a modal/dialog system that's accessible and reusable.

📌Requirements

  • Portal rendering (escape DOM hierarchy)
  • Focus trap
  • ESC key to close
  • Click outside to close
  • Prevent body scroll
  • Stacking multiple modals
  • 📌Portal Implementation

    const Portal = ({ children }) => {
    const [mounted, setMounted] = useState(false);
    useEffect(() => {
    setMounted(true);
    return () => setMounted(false);
    }, []);
    if (!mounted) return null;
    return createPortal(
    children,
    document.getElementById('modal-root')
    );
    };

    const Modal = ({ isOpen, onClose, title, children }) => {
    const modalRef = useRef();
    const previousFocus = useRef();
    useEffect(() => {
    if (isOpen) {
    previousFocus.current = document.activeElement;
    modalRef.current?.focus();
    document.body.style.overflow = 'hidden';
    }
    return () => {
    document.body.style.overflow = '';
    previousFocus.current?.focus();
    };
    }, [isOpen]);
    useEffect(() => {
    const handleEscape = (e) => {
    if (e.key === 'Escape') onClose();
    };
    if (isOpen) {
    document.addEventListener('keydown', handleEscape);
    }
    return () => document.removeEventListener('keydown', handleEscape);
    }, [isOpen, onClose]);
    if (!isOpen) return null;
    return (
    <Portal>
    <div className="modal-overlay" onClick={onClose}>
    <div
    ref={modalRef}
    className="modal-content"
    onClick={(e) => e.stopPropagation()}
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    tabIndex={-1}
    >
    <h2 id="modal-title">{title}</h2>
    {children}
    <button onClick={onClose}>Close</button>
    </div>
    </div>
    </Portal>
    );
    };

    📌Focus Trap

    const useFocusTrap = (ref, isActive) => {
    useEffect(() => {
    if (!isActive) return;
    const element = ref.current;
    const focusableElements = element.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];
    const handleTab = (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
    if (document.activeElement === firstElement) {
    e.preventDefault();
    lastElement.focus();
    }
    } else {
    if (document.activeElement === lastElement) {
    e.preventDefault();
    firstElement.focus();
    }
    }
    };
    element.addEventListener('keydown', handleTab);
    return () => element.removeEventListener('keydown', handleTab);
    }, [ref, isActive]);
    };

    const ModalContext = createContext();
    const ModalProvider = ({ children }) => {
    const [modals, setModals] = useState([]);
    const open = (modalId, props) => {
    setModals(prev => [...prev, { id: modalId, props }]);
    };
    const close = (modalId) => {
    setModals(prev => prev.filter(m => m.id !== modalId));
    };
    return (
    <ModalContext.Provider value={{ open, close, modals }}>
    {children}
    {modals.map((modal, index) => (
    <Modal key={modal.id} zIndex={100 + index} {...modal.props} />
    ))}
    </ModalContext.Provider>
    );
    };

    Asked at Google, Facebook, and Airbnb interviews.

    KS

    Written by

    Kavya Sharma

    Frontend Lead at Airbnb

    🚀 Master System Design

    Join 500+ developers

    Explore Courses →
    Chat on WhatsApp