fix: portal floating UI elements to document.body to prevent overflow clipping#2591
fix: portal floating UI elements to document.body to prevent overflow clipping#2591
Conversation
… clipping Floating UI elements (menus, toolbars, emoji picker) are now portaled to a dedicated container at document.body, preventing them from being clipped by overflow:hidden ancestors. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces a portal root infrastructure to render floating UI elements (menus, toolbars, popovers) outside the editor container, preventing clipping when the editor has overflow constraints. Concurrently, CSS selectors are shifted from Changes
Sequence DiagramsequenceDiagram
participant User
participant Editor as BlockNoteView
participant Context as BlockNoteContext
participant Popover as PopoverComponent
participant Portal as FloatingPortal
participant DOM as document.body
User->>Editor: Render editor
Editor->>DOM: createPortal(portalRootDiv)
Editor->>Context: Provide portalRoot in context
User->>Editor: Trigger floating UI (select text)
Editor->>Popover: Render Popover with context
Popover->>Context: Read portalRoot
Popover->>Portal: Render in FloatingPortal(portalRoot)
Portal->>DOM: Render floating UI outside editor
User->>Editor: Scroll/resize editor
Portal->>DOM: Floating UI updates via Floating UI
DOM->>User: Toolbar/menu visible & not clipped
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/mantine
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/extensions/SideMenu/SideMenu.ts (1)
615-626:⚠️ Potential issue | 🟡 MinorScope the
.bn-rootexemption to the current editor instance.
closest(".bn-root")now treats any BlockNote root as local UI. On multi-editor pages, a portaled toolbar from editor B hovering over editor A can keep editor A's side menu active because the target still has a.bn-rootancestor. This check needs an instance-scoped marker for the current editor/root pair, not a global class match.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/extensions/SideMenu/SideMenu.ts` around lines 615 - 626, The current check uses (event.target as HTMLElement).closest(".bn-root") which matches any editor root on the page; change it to an instance-scoped check so only the current editor/root keeps the side menu active. Use the SideMenu instance's root reference (e.g. this.rootElement or this.editorRoot) and replace the global closest(".bn-root") logic with an instance-scoped test such as using this.rootElement.contains(event.target as Node) or matching a unique root identifier/data-attribute (e.g. data-bn-root-id === this.rootId) on ancestor lookup; update the closestBNRoot usage and the conditional that references cursorWithinEditor so it only treats elements within this specific editor root as local UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/mantine/src/popover/Popover.tsx`:
- Line 26: Popover.tsx currently clears zIndex when portalRoot exists which lets
Mantine's default z-index win; change the zIndex logic to use the CSS variable
fallback pattern from GenericPopover.tsx by assigning zIndex to the CSS variable
--bn-ui-base-z-index with a 10000 fallback when portalRoot is truthy (and keep
10000 when not portaled) so portaled popovers respect the app's base z-index
system; update the zIndex prop usage in the Popover component (the zIndex prop
set at the repository diff line) accordingly to use the
var(--bn-ui-base-z-index, 10000) approach.
In `@packages/react/src/editor/BlockNoteView.tsx`:
- Around line 216-227: The portaled root created by createPortal currently only
forwards className and data-color-scheme, causing loss of important attributes
from the editor container; update the BlockNoteView portaling logic (the element
created where setPortalRoot is used) to derive and forward explicit props from
...rest (e.g., dir, any data-* attributes used for theming like data-theming-*,
data-mantine-color-scheme, and inline style/CSS variable overrides) instead of
relying on className alone, and avoid forwarding layout-only classes by either
whitelisting these attributes or picking them out from rest (preserve
editorColorScheme and className via mergeCSSClasses, but also copy dir, style,
and any data-* theming attributes into the portaled div so floating UI retains
same theme/context as the editor).
---
Outside diff comments:
In `@packages/core/src/extensions/SideMenu/SideMenu.ts`:
- Around line 615-626: The current check uses (event.target as
HTMLElement).closest(".bn-root") which matches any editor root on the page;
change it to an instance-scoped check so only the current editor/root keeps the
side menu active. Use the SideMenu instance's root reference (e.g.
this.rootElement or this.editorRoot) and replace the global closest(".bn-root")
logic with an instance-scoped test such as using
this.rootElement.contains(event.target as Node) or matching a unique root
identifier/data-attribute (e.g. data-bn-root-id === this.rootId) on ancestor
lookup; update the closestBNRoot usage and the conditional that references
cursorWithinEditor so it only treats elements within this specific editor root
as local UI.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c2308343-2a71-42d8-9eee-c0b1fdf5b028
📒 Files selected for processing (21)
docs/content/docs/react/styling-theming/themes.mdxexamples/01-basic/12-multi-editor/src/App.tsxexamples/02-backend/04-rendering-static-documents/src/App.tsxexamples/04-theming/02-changing-font/src/styles.cssexamples/04-theming/03-theming-css/src/styles.cssexamples/04-theming/04-theming-css-variables/src/styles.cssexamples/05-interoperability/09-blocks-to-html-static-render/src/App.tsxpackages/ariakit/src/comments/Comment.tsxpackages/ariakit/src/popover/Popover.tsxpackages/core/src/editor/editor.csspackages/core/src/extensions/SideMenu/SideMenu.tspackages/mantine/src/popover/Popover.tsxpackages/react/src/components/Comments/EmojiPicker.tsxpackages/react/src/components/Popovers/GenericPopover.tsxpackages/react/src/editor/BlockNoteContext.tspackages/react/src/editor/BlockNoteView.tsxpackages/react/src/editor/ComponentsContext.tsxpackages/react/src/editor/styles.csspackages/shadcn/src/comments/Comment.tsxpackages/shadcn/src/popover/popover.tsxplayground/src/style.css
💤 Files with no reviewable changes (1)
- packages/core/src/editor/editor.css
| onChange={onOpenChange} | ||
| position={position} | ||
| zIndex={10000} | ||
| zIndex={portalRoot ? undefined : 10000} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify z-index handling for Mantine popovers and bn root variable wiring.
# 1) Confirm changed Mantine popover behavior in this file.
rg -nP -C3 'withinPortal=|portalProps=|zIndex=' packages/mantine/src/popover/Popover.tsx
# 2) Check whether bn-ui-base-z-index is consumed anywhere in styling/code.
rg -nP -C2 'bn-ui-base-z-index|--bn-ui-base-z-index|z-index' packages/react packages/mantine
# 3) Inspect portal root creation context for explicit stacking styles.
rg -nP -C8 'createPortal\\(|className=|data-color-scheme|bn-root' packages/react/src/editor/BlockNoteView.tsxRepository: TypeCellOS/BlockNote
Length of output: 2993
🏁 Script executed:
# Find the portal root creation and its styling context
rg -nA15 -B5 'createPortal|portalRoot.*=' packages/react/src/editor/BlockNoteView.tsx | head -100Repository: TypeCellOS/BlockNote
Length of output: 2391
🏁 Script executed:
# Check if there's z-index styling on the portal root or bn-root specifically
rg -nP 'bn-root|portal.*root' packages/react/src/editor/styles.css packages/react/src/editor/BlockNoteView.tsx -A3 -B3Repository: TypeCellOS/BlockNote
Length of output: 4550
🏁 Script executed:
# Check Mantine Popover implementation or types to see how zIndex behaves
fd -t f -e tsx -e ts packages/mantine | xargs grep -l "zIndex" | head -5Repository: TypeCellOS/BlockNote
Length of output: 433
🏁 Script executed:
# Find Mantine Popover files
fd -t f --full-path 'packages/mantine' -e tsx -e ts | head -20Repository: TypeCellOS/BlockNote
Length of output: 856
🏁 Script executed:
# Check if there's any handling of zIndex prop for Popover in Mantine code
rg -n 'zIndex|z-index' packages/mantine/src/popover/ -B3 -A3Repository: TypeCellOS/BlockNote
Length of output: 538
🏁 Script executed:
# Verify what Mantine's default z-index behavior is for portaled popovers
rg -n 'const.*Popover|export.*Popover' packages/mantine/src/popover/ -A20 | head -80Repository: TypeCellOS/BlockNote
Length of output: 3408
Portaled popovers lose stacking control with Mantine's default z-index.
At line 26, zIndex is set to undefined when portalRoot exists. The portal root (created in packages/react/src/editor/BlockNoteView.tsx:217-226) has no explicit z-index styling and inherits --bn-ui-base-z-index: 0 from the .bn-root CSS rule. This causes Mantine to apply its default z-index (300 via CSS variables), which can fall behind fixed/sticky app chrome without explicit stacking control.
The fix aligns with the established pattern used in GenericPopover.tsx and other UI components, which leverage the --bn-ui-base-z-index CSS variable for consistent z-index management:
Proposed fix
- zIndex={portalRoot ? undefined : 10000}
+ zIndex={portalRoot ? "var(--bn-ui-base-z-index, 10000)" : 10000}This ensures portaled popovers respect the base z-index system while maintaining fallback to 10000 when the variable is not defined.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| zIndex={portalRoot ? undefined : 10000} | |
| zIndex={portalRoot ? "var(--bn-ui-base-z-index, 10000)" : 10000} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/mantine/src/popover/Popover.tsx` at line 26, Popover.tsx currently
clears zIndex when portalRoot exists which lets Mantine's default z-index win;
change the zIndex logic to use the CSS variable fallback pattern from
GenericPopover.tsx by assigning zIndex to the CSS variable --bn-ui-base-z-index
with a 10000 fallback when portalRoot is truthy (and keep 10000 when not
portaled) so portaled popovers respect the app's base z-index system; update the
zIndex prop usage in the Popover component (the zIndex prop set at the
repository diff line) accordingly to use the var(--bn-ui-base-z-index, 10000)
approach.
| {createPortal( | ||
| <div | ||
| ref={setPortalRoot} | ||
| className={mergeCSSClasses( | ||
| "bn-root", | ||
| editorColorScheme, | ||
| className, | ||
| )} | ||
| data-color-scheme={editorColorScheme} | ||
| />, | ||
| document.body, | ||
| )} |
There was a problem hiding this comment.
The portal root needs the same theme/context attrs as the editor root.
Only className and data-color-scheme are mirrored here. The editor container at Lines 207-213 still owns ...rest, so portaled UI loses things like dir, custom data-*, and inline CSS variable overrides. That means the new .bn-root[data-theming-*] demos and Mantine's data-mantine-color-scheme passthrough in packages/mantine/src/BlockNoteView.tsx:82-107 will not fully apply to floating UI. className alone is also not a safe proxy here, because layout classes can leak onto the body-level portal container. Please derive explicit portal-root props from ...rest instead of relying on className alone.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/editor/BlockNoteView.tsx` around lines 216 - 227, The
portaled root created by createPortal currently only forwards className and
data-color-scheme, causing loss of important attributes from the editor
container; update the BlockNoteView portaling logic (the element created where
setPortalRoot is used) to derive and forward explicit props from ...rest (e.g.,
dir, any data-* attributes used for theming like data-theming-*,
data-mantine-color-scheme, and inline style/CSS variable overrides) instead of
relying on className alone, and avoid forwarding layout-only classes by either
whitelisting these attributes or picking them out from rest (preserve
editorColorScheme and className via mergeCSSClasses, but also copy dir, style,
and any data-* theming attributes into the portaled div so floating UI retains
same theme/context as the editor).
Summary
Floating UI elements (slash menu, formatting toolbar, link toolbar, side menu, file panel, table handles, emoji picker) are now portaled to a dedicated container at
document.body, preventing them from being clipped byoverflow: hiddenancestors.Rationale
When BlockNote is rendered inside a container with
overflow: hidden(e.g., a sidebar, modal, or scrollable panel), floating UI elements get clipped and become partially or fully invisible. Portaling todocument.bodyis the standard fix.Closes #2543
Closes #2544
Closes #2558
Closes #2578
Supersedes the approach from #2092
Changes
Portal infrastructure
BlockNoteViewrenders a portal container atdocument.bodyviacreatePortal. This container getsbn-root+ color scheme + user className for theming, but NOTbn-container(layout only).portalRootexposed viaBlockNoteContextso all floating elements can access it.GenericPopover(the single component all floating UI elements flow through) wraps all render paths with<FloatingPortal root={portalRoot}>.CSS architecture:
bn-rootvsbn-containerbn-root: theming class (CSS variables, font-family). Applied to both the editor container and the portal container, so floating elements inherit the correct theme.bn-container: layout class (width, height). Only on the editor container.styles.cssmoved from.bn-containerto.bn-root..bn-rootbox-sizing reset from core (was never applied to any element).Popover
portalRootprop (for EmojiPicker)portalRootto the genericPopover.Rootcomponent interface.withinPortal+portalProps.portalRootvia React context from Root to Content, usescreatePortal(avoids modifying user's shadcn primitives).portalRootvia React context, uses nativeportalElementprop.Z-index handling
portalRoot, hardcodedz-index: 10000is dropped (unnecessary atdocument.bodylevel).portalRootis undefined from other call sites), the originalz-index: 10000is preserved.--bn-ui-base-z-indexon.bn-rootfor GenericPopover-based elements.EmojiPicker simplification
createPortal— now passesportalRoottoPopover.Rootand lets each UI library handle portaling.Minor fixes
emojiPickerOpenprop is now used (was previously ignored) to keep action buttons visible while emoji picker is open, matching mantine..closest(".bn-root")instead of editor wrapper containment check, so hovering portaled floating elements doesn't dismiss the side menu.em-emoji-pickerstyles to.bn-root, removed stalez-index: 11000.Impact
.bn-container[data-color-scheme]selectors need to update to.bn-root[data-color-scheme]..bn-containerstill exists but is for layout only.overflow: hiddenancestors.Testing
overflow: hiddencontainers.Checklist
Additional Notes
themeprops to verify theming works correctly on the portal container..closest(".bn-root")check is broader than the original editor-wrapper containment check, butmouseOverEditorbounding-box guard mitigates multi-editor edge cases.Summary by CodeRabbit
New Features
portalRootsupport for custom portal containers, enabling floating UI elements to render outside ancestor overflow constraints.themeprop to editor instances for explicit light/dark mode control.Bug Fixes
Documentation