Foundations
Portal
BetaSince 0.1.0
Renders its children into another DOM node, for content that can't use the top layer.
import { Portal } from "@anantya/uix";Overview
Portal renders its children into another DOM node, document.body unless you pass a container. It is for UIX components whose content must sit above the page and can't use the browser's top layer, such as Toast, coming in v1. In product code, use it only to render into another part of your own layout, such as a page's actions into the app header.
Building block
For a window, notice or menu over the page, use Dialog, or Toast, Popover or Dropdown Menu, coming in v1.
Missing a component? Ask for it instead of building your own.
Signature
function Portal(props: PortalProps): ReactPortal | null;
interface PortalProps {
children?: ReactNode;
container?: Element | DocumentFragment;
}Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | What it renders in the container. |
container | Element | DocumentFragment | document.body | The element or shadow root it renders into. |
Returns
Its children, rendered into the container once hydration is done, and nothing before that. In the React tree they stay where Portal is, so they keep its context, and their events bubble to its React parents.
Example
The invoices page renders its New invoice button into the header's actions with container.
It stays in the page's React tree, so it keeps the page's state and context.
Invoices
12 invoices this month
Code
"use client";
import { Add } from "@anantya/icons";
import { Button, Portal } from "@anantya/uix";
import { useState } from "react";
function InvoicesPage({ actions }: { actions: HTMLElement | null }) {
return (
<p className="p-4 text-sm text-muted-foreground">
12 invoices this month
{actions && (
<Portal container={actions}>
<Button size="sm" icon={<Add />}>
New invoice
</Button>
</Portal>
)}
</p>
);
}
export default function HeaderActions() {
const [actions, setActions] = useState<HTMLElement | null>(null);
return (
<div className="w-full max-w-md rounded-xl border border-border">
<div className="flex items-center justify-between gap-4 border-b border-border px-4 py-2">
<p className="text-sm font-semibold">Invoices</p>
<div ref={setActions} />
</div>
<InvoicesPage actions={actions} />
</div>
);
}Notes
- Portal renders nothing on the server or during hydration, and its children once hydration is done, so the server and client HTML always match. Content in a Portal isn't in the first paint
- Keep the container in reading order where the content belongs, as the header is for its actions: screen readers and Tab follow the DOM, not the React tree
- A Portal's content isn't announced on its own. Tell screen reader users what changed with
announce() - Content in a Portal inside a FocusTrap counts as inside the trap, so it can take focus
Changelog
Every change to Portal since 0.1.0 is in the release notes.