Configuring Portal Targets per Element
By default, BlockNote's floating UI elements (formatting toolbar, slash menu, table handles, etc.) mount inside the editor's bn-container element. The portalElements prop lets you change that — globally via default, or per element by key.
In this example we deliberately wrap the editor in a small parent with overflow: hidden so the global default of bn-container would clip the slash menu and the formatting toolbar. We escape only those two to document.body, while keeping tableHandles inside .bn-container so the table handles can never escape the editor's visual boundary.
<BlockNoteView
editor={editor}
portalElements={{
slashMenu: document.body,
formattingToolbar: document.body,
tableHandles: ".bn-container",
}}
/>Relevant Docs:
import "@blocknote/core/fonts/inter.css";import { BlockNoteView } from "@blocknote/mantine";import "@blocknote/mantine/style.css";import { useCreateBlockNote, type PortalElementsMap } from "@blocknote/react";import "./styles.css";const initialContent = [ { type: "paragraph" as const, content: "Click in this editor and press / to open the slash menu.", }, { type: "paragraph" as const, content: "Notice whether the menu fits inside the box or escapes it.", }, { type: "paragraph" as const, },];function PortalDemoEditor({ label, description, portalElements,}: { label: string; description: string; portalElements?: PortalElementsMap;}) { const editor = useCreateBlockNote({ initialContent }); return ( <div className="view-wrapper"> <div className="view-label">{label}</div> <div className="view-description">{description}</div> <div className="view"> <BlockNoteView editor={editor} portalElements={portalElements} /> </div> </div> );}export default function App() { return ( <div className="views"> <PortalDemoEditor label="Default — clipped" description="No portalElements prop. Floating UI mounts inside .bn-container — the slash menu is clipped by the editor's bounds." /> <PortalDemoEditor label="portalElements={{ default: document.body }} — escapes" description="Every floating UI element escapes the editor container and renders directly under <body>." portalElements={{ default: document.body }} /> </div> );}.views { container-name: views; container-type: inline-size; display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; padding: 8px;}/* * Each view is intentionally shorter than the slash menu so the clipping * vs escaping behaviour is visible at a glance. */.view-wrapper { display: flex; flex-direction: column; height: 260px; width: 100%;}@container views (width > 1024px) { .view-wrapper { width: calc(50% - 4px); }}.view-label { color: #0090ff; display: flex; font-size: 12px; font-weight: bold; justify-content: space-between; margin-inline: 16px;}.view-description { color: #0090ff; font-size: 12px; margin: 2px 16px 0;}/* * `position: relative` is what actually makes `overflow: hidden` clip the * absolutely-positioned floating UI. Without it the popover's containing * block is the viewport and the clip is bypassed. */.view { border: solid #0090ff 1px; border-radius: 16px; flex: 1; height: 0; padding: 8px; position: relative; overflow: hidden;}.view .bn-container { height: 100%; margin: 0; max-width: none; padding: 0;}.view .bn-editor { height: 100%; overflow: auto;}