@radix-ui/react-portal, @reach/portal, and react-portal are utilities designed to render React components into DOM nodes outside the current component hierarchy. This is essential for building modals, tooltips, and dropdowns that need to escape parent CSS constraints like overflow: hidden or z-index stacking contexts. While react-portal was an early solution before React 16, @reach/portal and @radix-ui/react-portal leverage modern React APIs to provide accessible and stable portal implementations within the component tree.
Rendering components outside their natural DOM hierarchy is a common requirement for overlays, modals, and tooltips. @radix-ui/react-portal, @reach/portal, and react-portal all solve this, but they differ significantly in maintenance status, API design, and alignment with modern React standards. Let's examine how they handle rendering, server-side rendering (SSR), and cleanup.
@radix-ui/react-portal uses a declarative component approach that fits naturally into modern React hooks and composition patterns. It renders children into a target container, defaulting to document.body.
// @radix-ui/react-portal
import * as Portal from '@radix-ui/react-portal';
function Modal() {
return (
<Portal.Root>
<div className="modal-content">Hello</div>
</Portal.Root>
);
}
@reach/portal also uses a component-based API, very similar in spirit to Radix. It is straightforward but offers fewer configuration options for advanced use cases.
// @reach/portal
import { Portal } from '@reach/portal';
function Modal() {
return (
<Portal>
<div className="modal-content">Hello</div>
</Portal>
);
}
react-portal relies on a legacy component API that was necessary before React 16. It requires importing the Portal component and passing children directly, but it lacks the modern safeguards and SSR optimizations found in newer libraries.
// react-portal
import Portal from 'react-portal';
function Modal() {
return (
<Portal>
<div className="modal-content">Hello</div>
</Portal>
);
}
@radix-ui/react-portal is built with SSR in mind. It avoids rendering the portal content on the server to prevent hydration mismatches, ensuring the client and server HTML match initially.
// @radix-ui/react-portal
// Handles SSR automatically by not rendering children on server
<Portal.Root>
<Content /> {/* Only renders on client */}
</Portal.Root>
@reach/portal supports SSR but requires careful configuration. It attempts to handle hydration gracefully, though edge cases may require manual intervention depending on the framework setup.
// @reach/portal
// Basic SSR support, but check hydration logs
<Portal>
<Content /> {/* Renders on client after hydration */}
</Portal>
react-portal has poor SSR support by modern standards. Since it was built before React's official SSR portal patterns were standardized, it often causes hydration warnings or errors in Next.js or Remix applications.
// react-portal
// Likely to cause hydration mismatches in modern frameworks
<Portal>
<Content /> {/* May trigger console warnings during hydration */}
</Portal>
@radix-ui/react-portal allows you to specify a custom container element. It handles cleanup automatically when the component unmounts, ensuring no orphaned nodes remain in the DOM.
// @radix-ui/react-portal
<Portal.Root container={document.getElementById('custom-root')}>
<div>Content</div>
</Portal.Root>
@reach/portal also supports a container prop and manages cleanup reliably. It is robust for standard use cases but offers less flexibility for complex container logic compared to Radix.
// @reach/portal
<Portal containerRef={document.getElementById('custom-root')}>
<div>Content</div>
</Portal>
react-portal supports custom containers but the API feels dated. Cleanup is generally handled, but the library does not account for some edge cases in concurrent React features introduced in React 18.
// react-portal
<Portal node={document.getElementById('custom-root')}>
<div>Content</div>
</Portal>
@radix-ui/react-portal is part of the actively maintained Radix UI ecosystem. It receives regular updates, bug fixes, and aligns with the latest React features. It is the recommended choice for new development.
@reach/portal is in maintenance mode. While not deprecated, development has slowed significantly. It is stable but may not keep pace with future React changes as quickly as Radix.
react-portal is effectively deprecated. React 16 introduced ReactDOM.createPortal, making this external library unnecessary for most use cases. It is not recommended for any new work.
| Feature | @radix-ui/react-portal | @reach/portal | react-portal |
|---|---|---|---|
| Status | โ Active | โ ๏ธ Maintenance | โ Legacy |
| SSR Support | โ Excellent | โ ๏ธ Good | โ Poor |
| API Style | Modern Component | Modern Component | Legacy Component |
| Accessibility | โ Built-in Focus | โ ๏ธ Basic | โ None |
| React 18 Ready | โ Yes | โ ๏ธ Mostly | โ No |
For any new project, @radix-ui/react-portal is the clear winner. It offers the best balance of modern API design, SSR safety, and active maintenance. It integrates perfectly with the rest of the Radix UI primitive ecosystem.
Use @reach/portal only if you are already invested in the Reach UI ecosystem and need consistency across existing components. Do not start new projects with it unless you have specific legacy constraints.
Avoid react-portal entirely. It solves a problem that React now solves natively. If you encounter it in an older codebase, plan to replace it with native ReactDOM.createPortal or @radix-ui/react-portal during your next refactor.
Choose @radix-ui/react-portal for new projects requiring accessible, unstyled primitives that integrate well with modern React patterns. It is actively maintained, supports server-side rendering, and works seamlessly with other Radix UI components. This is the safest bet for long-term stability and accessibility compliance.
Choose @reach/portal only if you are maintaining an existing codebase already dependent on Reach UI components. While functional, the Reach UI ecosystem has seen slower development compared to Radix, and new projects generally benefit from the more active community and feature set of Radix UI.
Do NOT use react-portal for new projects. It is considered legacy software since React 16 introduced built-in portal support via ReactDOM.createPortal. Existing projects using it should plan to migrate to native React portals or modern libraries like Radix to ensure compatibility with current React versions.
react-portalView docs here.