React Portal Utility

4.3.0 · active · verified Sun Apr 19

react-portal simplifies the creation and management of React Portals, enabling developers to render children into a different part of the DOM tree, outside the parent component's hierarchy. This is particularly useful for modals, tooltips, lightboxes, and notifications that require specific positioning or need to break out of CSS `overflow: hidden` containers. The current stable version is 4.3.0. It leverages React's official Portal API (introduced in React 16) to provide a robust solution. A key differentiator is its dual component approach, offering both a low-level `<Portal />` for maximum control and a `<PortalWithState />` for common stateful interactions (e.g., close on ESC, close on outside click) without external dependencies. Notably, since v4.1.0, it includes a fallback mechanism to support React v15 while primarily targeting React v16 and newer, making it flexible for diverse project ecosystems. It focuses on clean markup, SSR compatibility, and minimalistic design. The project demonstrates an active maintenance cadence, with recent minor releases addressing bugs and ensuring compatibility.

Common errors

Warnings

Install

Imports

Quickstart

Demonstrates both the basic `Portal` and the stateful `PortalWithState` components, showing how to render content into `document.body` or a custom DOM node, and handle portal visibility and interactions like closing on ESC or outside clicks.

import React, { useState, useEffect } from 'react';
import { Portal, PortalWithState } from 'react-portal';
import { createRoot } from 'react-dom/client';

const App = () => {
  const [isBasicPortalOpen, setIsBasicPortalOpen] = useState(false);

  // For demonstration purposes, create a target node if it doesn't exist
  useEffect(() => {
    if (typeof document !== 'undefined' && !document.getElementById('my-custom-portal-target')) {
      const div = document.createElement('div');
      div.id = 'my-custom-portal-target';
      document.body.appendChild(div);
    }
  }, []);

  return (
    <div>
      <h1>React-Portal Example</h1>

      <h2>Basic Portal</h2>
      <button onClick={() => setIsBasicPortalOpen(!isBasicPortalOpen)}>
        Toggle Basic Portal ({isBasicPortalOpen ? 'Open' : 'Closed'})
      </button>
      {isBasicPortalOpen && (
        <Portal>
          <div style={{
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            background: 'white',
            border: '2px solid blue',
            padding: '20px',
            zIndex: 1000,
            boxShadow: '0 4px 8px rgba(0,0,0,0.1)'
          }}>
            <p>This is a basic portal injected into <code>document.body</code>.</p>
            <button onClick={() => setIsBasicPortalOpen(false)}>Close</button>
          </div>
        </Portal>
      )}

      <h2>PortalWithState</h2>
      <PortalWithState closeOnOutsideClick closeOnEsc>
        {({ openPortal, closePortal, isOpen, portal }) => (
          <React.Fragment>
            <button onClick={openPortal} disabled={isOpen}>
              {isOpen ? 'Portal Open' : 'Open Advanced Portal'}
            </button>
            {portal(
              <div style={{
                position: 'fixed',
                top: '60%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                background: 'lightgreen',
                border: '2px solid darkgreen',
                padding: '25px',
                zIndex: 1001,
                boxShadow: '0 6px 12px rgba(0,0,0,0.15)'
              }}>
                <p>
                  This is a more advanced Portal. It handles its own state.
                  <br />
                  <button onClick={closePortal}>Close me!</button>, hit ESC or
                  click outside of me.
                </p>
                <p>Status: {isOpen ? 'Open' : 'Closed'}</p>
              </div>
            )}
          </React.Fragment>
        )}
      </PortalWithState>

      <h2>Custom Node Portal</h2>
      <p>This portal targets a custom div with id 'my-custom-portal-target'.</p>
      <Portal node={document && document.getElementById('my-custom-portal-target')}>
        <div style={{ border: '1px dashed orange', padding: '10px', marginTop: '10px' }}>
          Content portaled to #my-custom-portal-target.
        </div>
      </Portal>
    </div>
  );
};

// Mount the App to a root element
if (typeof document !== 'undefined') {
  const rootElement = document.getElementById('root');
  if (!rootElement) {
    const newRoot = document.createElement('div');
    newRoot.id = 'root';
    document.body.appendChild(newRoot);
  }
  const root = createRoot(document.getElementById('root')!); // Non-null assertion is safe after check
  root.render(<App />);
}

view raw JSON →