elastic / elastic/eui

[Epic] Reduce or eliminate unnecessary portalling in EUI components

Open
#9,503 2 comments 0 reactions 1 assignee Claimed by @tkajtoch View on GitHub
draft stale-issue
Dominant language
TypeScript
Stars
6.4k
Forks
911
Avg merge
2d 15h
Merged PRs (30d)
69

Description

### Summary / Pitch
**Target EUI release date(s)**: TBD

Portalling in EUI—rendering overlay components like flyouts, modals, and popovers into `document.body`—is causing UI crashes, React compatibility issues, and conflicts with Kibana's new layout architecture. This initiative reduces or eliminates unnecessary portalling to improve stability and unblock key Kibana features.

### Context

**Portalling was originally added to solve z-index and overflow issues**, but it has become a source of significant problems:

1. **UI Crashes** — Push flyouts throw `NotFoundError` during concurrent DOM operations when portal elements are removed mid-render (#9389). This crashes the UI with no recovery path.

2. **React Strict Mode Incompatibility** — Portal divs are created twice and orphaned divs aren't cleaned up, causing DOM bloat that accumulates with user interactions (#8784). This affects every EUI consumer using React 18's Strict Mode.

3. **Kibana Layout Blocked** — The new Kibana Workspace Chrome grid layout requires flyouts and popovers to be scoped to application containers, not `document.body`. Current portalling behavior prevents this (#8820).

4. **z-index Wars** — Portalled elements frequently appear above or below other UI elements unexpectedly, requiring constant workarounds (#4756, #4872, #5169, #4122).

5. **SSR Incompatibility** — Portal components fail during server-side rendering because `document` is not defined (#5656).

**This is not a new concern.** Issue #9255 explicitly states: *"Long term, consider what could be done about portaling generally in EUI, which is overused, unnecessary, and problematic."*

### Goals

- [ ] Eliminate unnecessary portalling where CSS-only solutions (e.g., `position: fixed`) suffice
- [ ] Provide container-scoped alternatives for components that currently portal to `document.body`
- [ ] Resolve React 18 Strict Mode compatibility issues
- [ ] Unblock Kibana Workspace Chrome layout requirements
- [ ] Reduce portal-related bug reports (currently 71+ issues filed)

### Out of scope

- Complete removal of all portalling (some edge cases may still require it)
- Changing the public API signatures of affected components (deprecation path should be gradual)
- Fixing third-party portal libraries (e.g., `react-reverse-portal`)

### Scope of Work

**12 components** currently rely on portalling:

| Component | Portal Method | Proposed Change |
|-----------|--------------|-----------------|
| `EuiPopover` | `EuiPortal` (always) | Evaluate if portalling is necessary; add `usePortal` opt-out |
| `EuiToolTip` | `EuiPortal` (always) | Evaluate if portalling is necessary; add `usePortal` opt-out |
| `EuiBottomBar` | `EuiPortal` (default on) | Change default to `usePortal={false}` |
| `EuiFlyout` | `EuiPortal` + `EuiOverlayMask` | Support container-scoped flyouts; reduce portal usage for push type |
| `EuiModal` | `EuiOverlayMask` → `EuiPortal` | Evaluate container-scoped alternative |
| `EuiDraggable` | `EuiPortal` (opt-in) | No change needed (already opt-in) |
| `EuiWrappingPopover` | `EuiPortal` (always) | Evaluate if portalling is necessary |
| `EuiImageFullScreenWrapper` | `EuiOverlayMask` | May need to remain portalled |
| `EuiCodeBlockFullScreen` | `EuiOverlayMask` | May need to remain portalled |
| `EuiDataGridCell` | `createPortal` | Evaluate if portalling is necessary |
| `_EuiPageBottomBar` | `createPortal` | No change needed (portals to parent, not body) |
| DataGrid Draggable Columns | `EuiPortal` | No change needed (drag overlay only) |

**High-level approach for each component:**
1. Audit current portal usage and determine if it's necessary
2. For components where portalling is unnecessary, remove it or change the default
3. For components where portalling is sometimes needed, add a `usePortal` prop (if not present)
4. For components where container-scoping is needed, add a `container` prop
5. Migrate `EuiPortal` from class to function component (#9456) to fix Strict Mode issues

### Definition of Ready for Dev
- [ ] PRD Ready
- Link: {link}
- [ ] Design Ready
- Design issue: N/A (infrastructure change)
- Design file: N/A
- [ ] Estimated
- [ ] Tasked

### Definition of done
- [ ] Kibana QA/preview completed (if needed)
- [ ] Changes made in EUI
- PRs: {link}
- [ ] EUI, Figma, and Documentation updates published.
- Figma: N/A
- EUI: {link}
- Documentation: {link}
- [ ] Request issues resolved and stakeholders informed of release.
- Links: {link}
- [ ] Changes applied to Kibana
- Links: {link}
- [ ] EUI Beta issue created (if applicable)
- Link: {link}
- [ ] EUI [Deprecation schedule](https://github.com/elastic/eui/issues/1469) updated (if applicable)

### Related Issues

### Notable Related Issue Details

#9255

> "Long term, consider what could be done about portaling generally in EUI, which is overused, unnecessary, and problematic."

This spike is investigating:
- How often users experience portal-related bugs in Kibana
- Short-term solutions (e.g., updating Kibana usage to `usePortal={false}`)
- Long-term improvements to portaling strategy in EUI

#9456

`EuiPortal` is being migrated from a class component to a function component:
- File: `components/portal/portal.tsx`
- Used internally by every overlay component (popover, modal, flyout, tooltip)
- Straightforward conversion: mount/unmount logic maps to `useEffect` cleanup pattern

#8784

When Strict Mode is enabled, portal divs are created twice and the empty one isn't cleaned up:
- Impacts all end-users
- No known workaround
- EuiPortal is used in: `EuiPopover` (512 uses in Kibana), `EuiToolTip` (856), `EuiFlyout` (350), `EuiOverlayMask` (16)

#7778

Request to allow `EuiPortal` to use different portal targets via React Context, instead of always using `document.body`. This would enable:
- Multi-window Electron apps
- Child windows opened via `window.open`
- Scoped flyouts/modals within specific containers

### Open Issues (High Priority)

| # | Title | Updated |
|---|-------|---------|
| [#9456](https://github.com/elastic/eui/issues/9456) | [EuiPortal] Migrate from class to function component | Mar 12, 2026 |
| [#9255](https://github.com/elastic/eui/issues/9255) | [Spike] Investigate "[EuiBottomBar] Fixed position..." | Feb 17, 2026 |
| [#9242](https://github.com/elastic/eui/issues/9242) | [EuiBottomBar] Fixed position shouldn't mount into a portal by default | Dec 2, 2025 |
| [#8784](https://github.com/elastic/eui/issues/8784) | [Strict Mode] Redundant div is portalled and not cleaned up | Dec 10, 2025 |
| [#8520](https://github.com/elastic/eui/issues/8520) | [EuiDataGrid][A11y] Column selector dragging accessibility improvements | Nov 27, 2025 |
| [#7778](https://github.com/elastic/eui/issues/7778) | EuiPortal should be able to use different portal target than document.body | Sep 22, 2025 |

Search results uncovering 50+ related issues

---

## Complete Issue List (50 issues returned)

| # | Title | State | Updated |
|---|-------|-------|---------|
| [#9389](https://github.com/elastic/eui/issues/9389) | [EuiFlyout] type="push" throws NotFoundError on unmount during concurrent DOM operations | Closed | Mar 12, 2026 |
| [#9456](https://github.com/elastic/eui/issues/9456) | [EuiPortal] Migrate from class to function component | **Open** | Mar 12, 2026 |
| [#9255](https://github.com/elastic/eui/issues/9255) | [Spike] Investigate "[EuiBottomBar] Fixed position... #9242" | **Open** | Feb 17, 2026 |
| [#8820](https://github.com/elastic/eui/issues/8820) | [Meta][Kibana Workspace Chrome] Improvements for new Kibana layout | Closed | Feb 5, 2026 |
| [#4157](https://github.com/elastic/eui/issues/4157) | [EuiDataGrid] Header Popover does not re-compute the position in scrollable container | Closed | Dec 15, 2025 |
| [#8784](https://github.com/elastic/eui/issues/8784) | [Strict Mode] Redundant div is portalled and not cleaned up | **Open** | Dec 10, 2025 |
| [#9242](https://github.com/elastic/eui/issues/9242) | [EuiBottomBar] Fixed position shouldn't mount into a portal by default | **Open** | Dec 2, 2025 |
| [#8520](https://github.com/elastic/eui/issues/8520) | [EuiDataGrid][A11y] Column selector dragging accessibility improvements | **Open** | Nov 27, 2025 |
| [#8984](https://github.com/elastic/eui/issues/8984) | [EuiPopover] Expose `repositionOnScroll` in `componentDefaults` | Closed | Nov 5, 2025 |
| [#8989](https://github.com/elastic/eui/issues/8989) | [Flyout System] Render `EuiOverlayMask` for `EuiFlyout` only when necessary | Closed | Oct 15, 2025 |
| [#7778](https://github.com/elastic/eui/issues/7778) | EuiPortal should be able to use different portal target than document.body based on provided Context | **Open** | Sep 22, 2025 |
| [#8578](https://github.com/elastic/eui/issues/8578) | Deprecate Support for React 16 | Closed | Jul 21, 2025 |
| [#8269](https://github.com/elastic/eui/issues/8269) | Workaround for Chromium CSS mask-image bug breaks positioning for nested position:fixed content | Closed | Apr 3, 2025 |
| [#8190](https://github.com/elastic/eui/issues/8190) | [EUI+] Missing Utilities content | Closed | Mar 17, 2025 |
| [#8410](https://github.com/elastic/eui/issues/8410) | [EUI+] Fix typos | Closed | Feb 27, 2025 |
| [#8206](https://github.com/elastic/eui/issues/8206) | [Bug] Focusable Site Navigation in Flyouts After Recent Update | Closed | Dec 5, 2024 |
| [#6300](https://github.com/elastic/eui/issues/6300) | [META] Add Cypress `cy.checkAxe()` tests to components | Closed | Nov 19, 2024 |
| [#5685](https://github.com/elastic/eui/issues/5685) | [Meta][CSS-in-JS] Component conversions | Closed | Sep 23, 2024 |
| [#6596](https://github.com/elastic/eui/issues/6596) | Portal widget (EuiBottomBar, EuiPopover etc) does not work in Vite dev mode | Closed | Aug 26, 2024 |
| [#7879](https://github.com/elastic/eui/issues/7879) | [A11Y] Using nested `EuiPopover` in `EuiModal`: Pressing ESC in child component closes parent | Closed | Jun 24, 2024 |
| [#7219](https://github.com/elastic/eui/issues/7219) | Spike: Removing Data Grid from @elastic/eui | Closed | Jun 20, 2024 |
| [#5788](https://github.com/elastic/eui/issues/5788) | [EuiPopover] Background in dark mode isn't different from the main background | Closed | May 30, 2024 |
| [#4265](https://github.com/elastic/eui/issues/4265) | [EuiPopover] Should unmount immediately on parent unmount | Closed | May 23, 2024 |
| [#7280](https://github.com/elastic/eui/issues/7280) | Close modal on outside click | Closed | May 14, 2024 |
| [#6882](https://github.com/elastic/eui/issues/6882) | [EuiPortal] Shadow DOM Support for Portal-Based Components (e.g., Modal, Overlay Mask) | Closed | May 13, 2024 |
| [#6845](https://github.com/elastic/eui/issues/6845) | [EuiToolTip] Dom cannot be removed | Closed | May 13, 2024 |
| [#6732](https://github.com/elastic/eui/issues/6732) | Not working properly on react and react-dom v-18 | Closed | Apr 17, 2024 |
| [#6592](https://github.com/elastic/eui/issues/6592) | [EuiPopover] Scrolling an open popover down stretches page height | Closed | Mar 21, 2024 |
| [#5330](https://github.com/elastic/eui/issues/5330) | Guidelines for bottom bars | Closed | Feb 12, 2024 |
| [#6338](https://github.com/elastic/eui/issues/6338) | EuiModal scrolls to bottom of page when modal is open | Closed | Jan 30, 2024 |
| [#6337](https://github.com/elastic/eui/issues/6337) | (duplicate of #6338) | Closed | Jan 30, 2024 |
| [#6304](https://github.com/elastic/eui/issues/6304) | EuiModal scrolls to bottom of page when modal is open | Closed | Jan 30, 2024 |
| [#5370](https://github.com/elastic/eui/issues/5370) | [EuiMarkdown] Allow `EuiFlyout` instead of `EuiModal` | Closed | Dec 6, 2023 |
| [#6339](https://github.com/elastic/eui/issues/6339) | [EuiResizableContainer] Add support for `onResizeStart` and `onResizeEnd` callback props | Closed | Nov 14, 2023 |
| [#6225](https://github.com/elastic/eui/issues/6225) | [EuiPortal docs] Add a custom modal/flyout example that meets a11y requirements | Closed | Sep 27, 2023 |
| [#6241](https://github.com/elastic/eui/issues/6241) | [EuiResizableContainer] Resizable container misbehaves when using react-reverse-portal | Closed | Aug 31, 2023 |
| [#6199](https://github.com/elastic/eui/issues/6199) | [EuiResizableContainer] Resizable container misbehaves when using react-reverse-portal | Closed | Aug 31, 2023 |
| [#5859](https://github.com/elastic/eui/issues/5859) | Popover and ContextMenu do not support react 18 | Closed | May 11, 2023 |
| [#5656](https://github.com/elastic/eui/issues/5656) | [EuiBottomBar] document is not defined when using server rendering | Closed | Apr 26, 2023 |
| [#1101](https://github.com/elastic/eui/issues/1101) | [EuiContextMenu] Tabbing regressions | Closed | Mar 23, 2023 |
| [#4364](https://github.com/elastic/eui/issues/4364) | [EuiDataGrid] Columns resize infinite loop | Closed | Jan 24, 2023 |
| [#4872](https://github.com/elastic/eui/issues/4872) | [EuiComboBox] Combobox popover can be hidden behind EuiBottomBar | Closed | Dec 19, 2021 |
| [#4756](https://github.com/elastic/eui/issues/4756) | [EuiPopover] Popover is placed above header (z-index) | Closed | Nov 29, 2021 |
| [#2903](https://github.com/elastic/eui/issues/2903) | Scrollspy feature requested | Closed | Sep 24, 2021 |
| [#5169](https://github.com/elastic/eui/issues/5169) | EuiGlobalToastList became hidden behind EuiFlyout with EUI 7.14.1 | Closed | Sep 22, 2021 |
| [#1023](https://github.com/elastic/eui/issues/1023) | [EuiInputPopover] In fixed container (like flyout) has positioning issues | Closed | Sep 13, 2021 |
| [#4666](https://github.com/elastic/eui/issues/4666) | [Portals][Doc] Props table not available for Portals | Closed | Mar 29, 2021 |
| [#1381](https://github.com/elastic/eui/issues/1381) | [makeId] Makes it difficult for consumers to create Jest tests | Closed | Nov 11, 2020 |
| [#4122](https://github.com/elastic/eui/issues/4122) | [EuiFlyout] [EuiButtonGroup] Z-Index conflict when a Flyout occludes Toggle Buttons | Closed | Oct 8, 2020 |
| [#1877](https://github.com/elastic/eui/issues/1877) | EuiBottomBar needs a `container` prop similar to EuiPopover | Closed | Sep 20, 2020 |

### Risks

- **Breaking changes** — Changing portal defaults may affect existing Kibana usage; requires careful deprecation path
- **Edge cases** — Some components may genuinely need portalling in certain contexts (e.g., deeply nested scroll containers)
- **Testing complexity** — Portal behavior is difficult to test; changes require extensive manual QA in Kibana

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.