React Portals

Render child components into external DOM nodes outside parent hierarchy using createPortal without losing React context.

TL;DR

  1. Teleport elements into distant DOM containers using createPortal.
  2. Escape restrictive CSS overflow and z-index stacking boundaries easily.
  3. Preserve React context and synthetic event bubbling across portal gaps.

Portal Fundamentals

    createPortal API

    Mount JSX into specific external DOM target element.

    import { createPortal } from 'react-dom';
    
    export function Overlay({ children }: Props) {
      return createPortal(children, document.body);
    }
    Mounted DOM Check

    Ensure container node exists before rendering portal.

    const [mounted, setMounted] = useState(false);
    useEffect(() => {
      setMounted(true);
    }, []);
    if (!mounted) return null;
    return createPortal(children, document.body);
    Dynamic Target Container

    Create and clean up dedicated mount element dynamically.

    useEffect(() => {
      const el = document.createElement('div');
      document.body.appendChild(el);
      return () => { document.body.removeChild(el); };
    }, []);

Event Bubbling and Context

    Synthetic Event Bubbling

    Parent catches clicks originating inside portal child.

    export function Shell() {
      const onPortalClick = () => {
        console.log('Clicked in portal');
      };
      return (
        <CardView onClick={onPortalClick}>
          <PortalModal />
        </CardView>
      );
    }
    Context Transparency

    Context passes seamlessly through portal boundaries.

    export function PortalContent() {
      const theme = useContext(ThemeContext);
      return (
        <Text color={theme.accent}>Portaled</Text>
      );
    }
    Escape Overflow Constraints

    Break free from ancestor hidden overflow clipping.

    // Renders outside overflow: hidden container
    return createPortal(
      <TooltipView text="Info" />,
      document.body
    );

Floating Overlays and Tooltips

    Positioned Floating Popover

    Render absolute floating panel using measured coords.

    return createPortal(
      <PopoverView style={{ top: y, left: x }}>
        <Text>Popover</Text>
      </PopoverView>,
      document.body
    );
    Backdrop Overlay Mask

    Render dark backdrop behind modal outside hierarchy.

    return createPortal(
      <Backdrop onClick={onClose} />,
      document.body
    );
    Multiple Dedicated Portal Roots

    Separate portal targets for modals and tooltips.

    <PortalRoot id="modal-root" />
    <PortalRoot id="tooltip-root" />

Portal Best Practices

    Accessibility Dialog Roles

    Apply proper ARIA role and modal state attributes.

    <DialogView
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    />
    Escape Key Handling

    Listen for escape keydown to close overlay.

    useEffect(() => {
      const onEsc = (e: KeyboardEvent) => {
        if (e.key === 'Escape') onClose();
      };
      window.addEventListener('keydown', onEsc);
      return () => {
        window.removeEventListener('keydown', onEsc);
      };
    }, [onClose]);
    Focus Restoration

    Return focus to trigger button upon portal close.

    const prevFocus = useRef(document.activeElement);
    useEffect(() => () => {
      (prevFocus.current as HTMLElement)?.focus();
    }, []);

Tips

  1. Mount modals and floating tooltips directly into document.body to avoid clipping issues caused by ancestor overflow: hidden rules.
  2. Take advantage of React synthetic event bubbling through portals to capture child interactions in ancestor onClick handlers as normal.

Warnings

  1. Always verify that the target DOM element exists before invoking createPortal to prevent null container mounting crashes.
  2. Remember to manage keyboard focus trapping and aria-modal attributes when rendering dialogs into external portal nodes.

In Practice

FAQ