Portal
A component that renders its children into a new DOM node outside the parent hierarchy.
Portal
Renders children into a DOM node outside the parent hierarchy.
document.body — outside this card — yet declared here in the render tree.import { useState } from 'react';
import { Portal } from '@/components/portal';
import { Surface } from '@/components/surface';
export const meta = { layout: 'fullscreen' } as const;
export default function PortalExample() {
const [container, setContainer] = useState<HTMLElement | null>(null);
return (
<div className="flex min-h-dvh flex-col items-center justify-center gap-6 p-8">
<Surface elevation="raised" className="w-full max-w-sm">
<Surface.Header>
<Surface.Title>Portal</Surface.Title>
<Surface.Description>
Renders children into a DOM node outside the parent hierarchy.
</Surface.Description>
</Surface.Header>
<Surface.Content className="text-foreground-secondary">
The banner below is portalled into{' '}
<code className="font-mono text-foreground text-xs">
document.body
</code>{' '}
— outside this card — yet declared here in the render tree.
</Surface.Content>
</Surface>
{/* Custom container — Portal targets this local element. */}
<Surface
elevation="sunken"
className="flex w-full max-w-sm items-center justify-center"
>
<div
ref={setContainer}
className="grid min-h-20 w-full place-items-center"
/>
{container ? (
<Portal container={container}>
<p className="text-foreground-secondary text-sm">
Portalled into a local container ref.
</p>
</Portal>
) : null}
</Surface>
{/* Default container — Portal renders into document.body. */}
<Portal>
<div
role="status"
aria-label="Portalled banner"
className="fixed inset-x-0 top-0 z-40 bg-foreground px-4 py-2 text-center text-background text-sm"
>
This banner is rendered at the end of document.body.
</div>
</Portal>
</div>
);
} Dependencies
Source Code
'use client';
import { useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
type PortalProps = {
container?: Element;
children: React.ReactNode;
};
export const Portal = (props: PortalProps) => {
const [mounted, setMounted] = useState(false);
useLayoutEffect(() => setMounted(true), []);
const container = props.container || (mounted && document.body);
return container ? createPortal(props.children, container) : null;
};
export type { PortalProps }; Features
- DOM Rendering: Renders content at the end of document.body by default
- Custom Container: Supports rendering into any DOM element
- SSR Compatible: Works seamlessly with server-side rendering
- Type Safe: Full TypeScript support for props and children
- Lightweight: Uses React’s built-in createPortal API
API Reference
Portal
| Prop | Default | Type | Description |
|---|---|---|---|
container | document.body | Element | The DOM element to render the portal into. |
children | - | React.ReactNode | The content to render inside the portal. |
Examples
Default
A portal rendering into document.body alongside one targeting a local container.
Portal
Renders children into a DOM node outside the parent hierarchy.
document.body — outside this card — yet declared here in the render tree.import { useState } from 'react';
import { Portal } from '@/components/portal';
import { Surface } from '@/components/surface';
export const meta = { layout: 'fullscreen' } as const;
export default function PortalExample() {
const [container, setContainer] = useState<HTMLElement | null>(null);
return (
<div className="flex min-h-dvh flex-col items-center justify-center gap-6 p-8">
<Surface elevation="raised" className="w-full max-w-sm">
<Surface.Header>
<Surface.Title>Portal</Surface.Title>
<Surface.Description>
Renders children into a DOM node outside the parent hierarchy.
</Surface.Description>
</Surface.Header>
<Surface.Content className="text-foreground-secondary">
The banner below is portalled into{' '}
<code className="font-mono text-foreground text-xs">
document.body
</code>{' '}
— outside this card — yet declared here in the render tree.
</Surface.Content>
</Surface>
{/* Custom container — Portal targets this local element. */}
<Surface
elevation="sunken"
className="flex w-full max-w-sm items-center justify-center"
>
<div
ref={setContainer}
className="grid min-h-20 w-full place-items-center"
/>
{container ? (
<Portal container={container}>
<p className="text-foreground-secondary text-sm">
Portalled into a local container ref.
</p>
</Portal>
) : null}
</Surface>
{/* Default container — Portal renders into document.body. */}
<Portal>
<div
role="status"
aria-label="Portalled banner"
className="fixed inset-x-0 top-0 z-40 bg-foreground px-4 py-2 text-center text-background text-sm"
>
This banner is rendered at the end of document.body.
</div>
</Portal>
</div>
);
} Basic Usage
A simple example showing how to render content in a portal.
import { Portal } from "@/foundations/ui/portal/portal";
export default function PortalExample() {
return (
<Portal>
<div className="fixed inset-0 bg-black/50">This content is rendered at the end of document.body</div>
</Portal>
);
}
Custom Container
Rendering content into a specific DOM element.
import { Portal } from "@/foundations/ui/portal/portal";
import { useRef } from "react";
export default function CustomContainerExample() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div>
<div ref={containerRef} className="relative min-h-[100px]" />
<Portal container={containerRef.current}>
<div className="absolute inset-0 flex items-center justify-center">
This content is rendered inside the container div
</div>
</Portal>
</div>
);
}
Best Practices
-
Accessibility: When using portals, ensure that:
- The portal content is properly labeled with ARIA attributes
- Focus management is handled correctly
- The content is properly announced to screen readers
-
Event Bubbling: Remember that events still bubble up through React’s virtual DOM hierarchy, not the actual DOM hierarchy.
-
SSR Considerations: The portal only renders on the client side. Make sure your code handles the server-side case appropriately.
-
Z-Index Management: When using multiple portals, carefully manage z-index values to ensure proper stacking order.
Previous
Popover
Next
Progress