iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Portals

React portals render children into a different DOM node. Modals, tooltips, toasts: escape the parent stacking context.

React — portals

EXAMPLE
// ===== createPortal =====
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="fixed inset-0 grid place-items-center bg-black/50" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} className="bg-white p-6 rounded">
        {children}
      </div>
    </div>,
    document.body
  );
}

// Usage:
function App() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <Modal open={open} onClose={() => setOpen(false)}>Hello</Modal>
    </>
  );
}

// ===== Why portals =====
// - Escape parent overflow: hidden / opacity / transform (which create stacking contexts)
// - Avoid z-index wars; render at body root
// - Events still bubble through React's virtual tree (not the DOM tree)

// ===== Event bubbling quirk =====
// A click inside the portal bubbles up through the React parent, not the DOM parent.
// Useful: parent onClick handlers still fire.
// Surprise: stopPropagation in the portal still affects the React parent.

// ===== A toast container =====
function ToastContainer({ messages }) {
  return createPortal(
    <ol className="fixed top-4 right-4 space-y-2">
      {messages.map(m => <li key={m.id} className="bg-slate-900 text-white p-3 rounded">{m.text}</li>)}
    </ol>,
    document.body
  );
}

// ===== SSR considerations =====
// document.body does not exist server-side. Guard:
const target = typeof document !== 'undefined' ? document.body : null;
return target ? createPortal(content, target) : null;

// Or use a framework helper (Next.js dynamic import with ssr: false).

// ===== Custom target =====
const target = document.getElementById('portal-root');
// In index.html: <div id="portal-root"></div>

// ===== Patterns to internalise =====
// - Portal modals, tooltips, popovers, toasts
// - Stop propagation INSIDE the portal content if clicks should not close
// - Guard for SSR (document undefined)
// - Centralise a <PortalRoot /> component for consistent target

// ===== Pitfalls =====
// - Portal inside a parent with transform/opacity — portal DOES escape; the parent doesn't
// - Forgetting to restore focus to the trigger on close (accessibility)
// - Scroll lock on body when modal is open (handle manually or via lib)
// - Multiple portals fighting for the same root without ordering

Why it matters

createPortal escapes the DOM tree without leaving the React tree. Modals, toasts, tooltips all benefit. Render at body root, manage focus + scroll lock, and stop propagation inside content when clicks should not close.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
createPortal(<Modal />, document.body)
Try it Yourself »

Discussion

Loading…