Skip to main content

Portal

Renders a node into another part of the document, after mount, keeping it inside the React tree. It is the escape hatch the kit's own overlays use to get out of overflow: hidden.

PortalPortal

Use this when / not when​

  • Use for something of yours that has to escape a clipping or stacking ancestor: a popover anchored to a cell, a layer over a scrolling region.
  • Not for the kit's overlays. ModalDialog, DropDown and Tooltip already render through one; wrapping them again does nothing useful.
  • Not to position anything. This component renders no element and no styles — the node you pass positions itself.
  • Not in a server-rendered tree, when the content has to be in the first HTML: it renders nothing on the server and nothing on the first client render.

Import​

import { Portal } from "@onlyoffice/apps-ui-kit/components/portal";

Also exported from the root barrel @onlyoffice/apps-ui-kit.

No provider needed.

Stories​

Default​

The content passed in element shows inside the dashed container that owns it, not beside the text it was declared next to (appendTo). Switch visible in the Controls panel below to unmount and mount it again.

DefaultDefault

Hidden​

The container stays empty: with visible off the content is not hidden but not mounted at all, so nothing of it reaches the DOM.

HiddenHidden

Custom Container​

Portal rendering into a specific custom container element instead of document.body.

Custom ContainerCustom Container

Multiple Portals​

Three portals share one container: each is appended after the last, and the portals do nothing about overlap, so every node carries its own position.

Multiple PortalsMultiple Portals

Toggle Visibility​

Click Show Portal and Close to open and close the content from outside and from inside it. Each close unmounts the content (visible), so state held inside it starts over on the next open.

Toggle VisibilityToggle Visibility

Into Document Body​

With no appendTo, the content leaves the dashed box it is declared in and lands at the end of the page body, centred on the window by its own fixed position.

Into Document BodyInto Document Body

Css Customization​

Portal has no styles of its own, so the README lists no CSS variables for it: the --portal-popup-* variables set here belong to this page's demo popup, and are set on the container the portal appends to so the content picks them up there.

Css CustomizationCss Customization

Minimal example​

The content goes in element, not in children, and lands at the end of document.body.

import { Portal } from "@onlyoffice/apps-ui-kit/components/portal";

export function FloatingNotice({ text }: { text: string }) {
return (
<Portal
element={
<div
style={{
position: "fixed",
insetBlockEnd: 16,
insetInlineEnd: 16,
zIndex: 500,
}}
>
{text}
</div>
}
/>
);
}

Props​

PropertyTypeDescription
elementReactNodeWhat to render in the container. It is a node, not children: the component renders nothing of its own around it.
appendTo?HTMLElement | nullElement to append the content to. It is read on every render, so a value that is null on the first render — a ref — falls back to document.body; hold the container in state instead. Default: null.
visible?booleanWhether the content is rendered. Turning it off unmounts the content, so anything it held is lost. Default: true.

Recipes​

Open and close, controlled​

import { useState } from "react";
import { Button } from "@onlyoffice/apps-ui-kit/components/button";
import { Portal } from "@onlyoffice/apps-ui-kit/components/portal";

export function DetailsLayer() {
const [open, setOpen] = useState(false);

return (
<>
<Button label="Details" onClick={() => setOpen(!open)} />
<Portal
visible={open}
element={
<div
style={{ position: "fixed", inset: "20% 20% auto", zIndex: 500 }}
>
<Button label="Close" onClick={() => setOpen(false)} />
</div>
}
/>
</>
);
}

Into a container of your own​

The container has to exist when the portal renders, so keep it in state rather than in a ref.

import { useState } from "react";
import { Portal } from "@onlyoffice/apps-ui-kit/components/portal";

export function SidebarSlot({ label }: { label: string }) {
const [container, setContainer] = useState<HTMLDivElement | null>(null);

return (
<div>
<div ref={setContainer} style={{ position: "relative" }} />
<Portal appendTo={container} element={<span>{label}</span>} />
</div>
);
}

Behaviour the types don't state​

  • It renders nothing until after mount. Mounting is recorded in an effect, so the first render — and the whole server render — produces null, and the content appears one render later. Anything that measures it has to wait a frame.
  • appendTo is read at render time and not remembered. A ref whose current is still null on that render silently falls back to document.body, which is why the container belongs in state.
  • visible={false} unmounts the content rather than hiding it, so state inside it is lost and any animation out has to happen before you flip the prop.
  • The node is appended at the end of the container, after everything already in it, and nothing is done about stacking — give it a z-index of its own.
  • React context still reaches the content: providers above the Portal apply inside it, and so does event bubbling through the React tree, though not through the DOM.
  • The component renders no wrapper element, so there is nothing to give a class or a test id to.

Accessibility​

  • The content leaves its place in the DOM order, so screen-reader and tab order follow document.body, not your layout. Move focus into it deliberately when it opens and back when it closes.
  • Nothing here marks the rest of the page inert or handles Escape. A layer that must be modal needs ModalDialog, which does both.

Test ids​

The component renders no element of its own, so it carries no data-testid. Put one on the node you pass as element.

  • ModalDialog — a dialog that already portals itself.
  • DropDown — a menu that portals unless you turn it off.
  • Tooltip — always portalled, into document.body.