Skip to content

[Flyouts] Add container prop for app-level or global flyouts + fixes for resizable flyouts#9377

Open
tsullivan wants to merge 53 commits intoelastic:mainfrom
tsullivan:flyout/resizing-and-containers
Open

[Flyouts] Add container prop for app-level or global flyouts + fixes for resizable flyouts#9377
tsullivan wants to merge 53 commits intoelastic:mainfrom
tsullivan:flyout/resizing-and-containers

Conversation

@tsullivan
Copy link
Member

@tsullivan tsullivan commented Feb 10, 2026

Summary

Closes #9144
Unblocks elastic/kibana#242610

Why are we making this change?

This PR addresses two related issues with the EUI Flyout System:

  1. Resizable flyouts cause strange stacking effects (1️⃣ [Flyout System] resizable flyouts cause strange stacking effects #9144) — When a main or child flyout in the Flyout System is resizable, the user can drag the resize handle to a point where the layout mode switches from side-by-side to stacked. This causes content to shift around mid-drag, creating a confusing experience. The fix introduces sibling-aware clamping so the resize handle stops before the combined flyout width triggers a layout mode change.

  2. Child flyouts have wrong positioning when a sidebar is visible ([Grid Layout / Flyout System] When sidebar is visible, child flyouts have wrong positioning kibana#242610) — In Kibana's grid layout with sidebars enabled, child flyouts are positioned with an incorrect offset because the flyout system assumes it owns the full viewport width. The flyout's position: fixed and vw-based sizing do not account for the sidebar taking up horizontal space, causing child flyouts to be misaligned relative to their parent.

Both issues are interconnected: container-scoped flyouts need container-aware resize clamping, and the resize clamping fix requires a reference width that can be either the container or the viewport. The implementation solves both problems together through a shared concept of a "reference width" that all flyout sizing and layout calculations use.

How it works

container prop

An optional container prop is added to EuiFlyout. When provided, the container element is used as a reference element for positioning and dimension calculations.

  • The prop can also be set globally via EuiProvider component defaults: it is recommended to set the default to the application container for convenience.
  • Passing container={null} on a child forces it to use document.body as the reference container element.
  • When the container prop is undefined, the flyout uses standard viewport-relative position: fixed behavior — no container-relative positioning styles are applied. This is how these changes avoid becoming breaking.

Resize hook improvements

useEuiFlyoutResizable now:

  • Outputs percentage-based widths relative to a reference width (container or viewport)
  • Clamps at 90% of the reference width to prevent flyouts from covering the entire container/viewport
  • Accounts for sibling flyout widths so the resize handle stops before the combined width would trigger a layout mode change
  • Scales proportionally when the container or viewport resizes (both shrink and grow)

Fill-size flyout coordination

When one flyout uses size="fill" alongside a fixed-size sibling in side-by-side mode, the fill flyout dynamically takes the remaining available space (90% of container minus the sibling's width). This works symmetrically — fill main + fixed child, or fixed main + fill child.

Deprecations

  • maskProps: Deprecated in favor of the container prop. Use container to scope flyouts to an application area, or container={null} for a true global (viewport-relative) flyout. Previously, maskProps.headerZindexLocation was often used to place a flyout above or below a fixed header; with container, positioning is derived from the container. When container is provided, maskProps is ignored and a console warning is emitted in development.
  • includeFixedHeadersInFocusTrap: Deprecated when using the container prop. For app-level flyouts, use includeSelectorInFocusTrap to include specific elements (e.g. app headers) in the focus trap. When container is provided, includeFixedHeadersInFocusTrap is ignored and a console warning is emitted in development.

Screenshots

flyout-with-container-prop-test.mp4

Impact to users

  • Usability: support for scoping flyouts to a container element, distinguishing app-level flyouts from global flyouts
  • Accessibility fix: content in a resized flyout will no longer render offscreen if the viewport is resized
  • Performance fix: no more 1-frame lag in synchronizing child flyout position when resizing a main flyout
  • Bug fix: resizable flyouts can no longer be dragged past 90% of the reference width, preventing unintended layout mode switches

Testing

This PR adds a new "Container demo" storybook to the Flyout offering of storybook. To test these changes in Kibana, try the following:

  1. Build this branch into an EUI package.
  2. Install in Kibana
  3. Make these supplementary changes in Kibana: https://github.com/tsullivan/kibana/pull/new/flyouts/default-container-prop
    • NOTE: This is a demo branch that also includes the setup of a custom build of EUI. If you have your own build of EUI for testing this, use that instead.
  4. Run Kibana with yarn start --run-examples
  5. These changes fix issues with flyouts when a sidebar is open. Open a side bar using the Sidebar Examples page: /app/sidebarExamples
  6. Now, navigate to the Flyout System Examples page: /app/flyoutSystemExamples
  7. Test the flyout with a sidebar in the example app
  8. Test other flyouts in Kibana to make sure there are no regressions (no breaking changes)

QA

Remove or strikethrough items that do not apply to your PR.

General checklist

  • Browser QA
    • Checked in both light and dark modes
    • Checked in both MacOS and Windows high contrast modes
    • (emulate forced colors if you do not have access to a Windows machine.)
    • Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
    • Checked for accessibility including keyboard-only and screenreader modes
  • Docs site QA
    • Added documentation
    • Props have proper autodocs (using @default if default values are missing) and playground toggles
    • Checked Code Sandbox works for any docs examples
  • Code quality checklist
    • Added or updated jest and cypress tests
    • Updated visual regression tests
  • Release checklist
    • A changelog entry exists and is marked appropriately
    • If applicable, added the breaking change issue label (and filled out the breaking change checklist)
    • If the changes unblock an issue in a different repo, smoke tested carefully (see Testing EUI features in Kibana ahead of time)
  • Designer checklist
    • If applicable, file an issue to update EUI's Figma library with any corresponding UI changes. (This is an internal repo, if you are external to Elastic, ask a maintainer to submit this request)

tsullivan and others added 4 commits February 12, 2026 17:14
Refactor `useEuiFlyoutResizable` to output percentage-based widths
instead of raw pixels. The hook now clamps resize at 90% of a
reference width (defaulting to the viewport), accounts for a sibling
flyout's width, and scales proportionally when the container resizes
in either direction. Updates unit and Cypress tests accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rlay

Add `containerElement` and `minWidth` to the flyout manager state,
actions, reducer, and store. Extend `EuiPortal` with an optional
`container` prop for mounting into a specific element. Enrich
`EuiFlyoutParentContext` to share the parent's container element
with child flyouts. Add `container` prop to the flyout overlay for
portal targeting. Refactor selector tests to use a `createMockManager`
factory that includes the new `setContainerElement` API.

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce the `container` prop on `EuiFlyout` for positioning flyouts
relative to an application container instead of the viewport. When a
container is provided, the flyout uses `position: absolute` with
container queries; otherwise it uses `position: fixed` with media
queries. Width values use `%` units throughout (identical to `vw` for
fixed positioning, container-relative for absolute).

Key changes:
- Refactor `euiFlyoutStyles` into shared base + position variants
  (`fixed`/`absolute`) + sizing modes (`viewport`/`container`)
- Wire container through `EuiFlyoutParentContext` for child inheritance
- Set `container-type: inline-size` and scroll-reset listener on the
  container element; report it to manager state for layout calculations
- `layout_mode.ts` uses container width for responsive breakpoints
- Add `container` to `EuiProvider` component defaults for global config
- Consolidate selector hooks: read `useFlyoutManager()` once per
  component and derive values inline (reduces redundant calls)
- Deprecation warnings for `maskProps` and
  `includeFixedHeadersInFocusTrap` when `container` is provided
- `--euiFlyoutMainWidth` CSS variable for synchronous fill-child sizing

Co-authored-by: Cursor <cursoragent@cursor.com>
Add website documentation for the `container` prop explaining
container-relative positioning, `position: relative` requirement,
and the `container-type: inline-size` caveat for fixed descendants.

Add `FlyoutContainerDemo` storybook demonstrating both "app" flyouts
(container-relative, push type, with child) and "global" flyouts
(viewport-relative, overlay type) in a simulated app layout with
left navigation and right tools sidebar.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tsullivan tsullivan force-pushed the flyout/resizing-and-containers branch from 31eb10d to 57861f6 Compare February 13, 2026 00:28
@tsullivan tsullivan requested a review from a team as a code owner February 20, 2026 21:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the EUI Flyout system by adding container-scoped (app-level) flyouts and improving resizable flyout behavior to prevent layout mode “stacking” glitches and sizing drift when the reference width changes.

Changes:

  • Added an optional container prop (and provider defaults support) to scope flyout positioning/sizing to an app container instead of the viewport.
  • Updated resizable flyout logic to use reference-width-relative percentages and clamp widths (including sibling-aware clamping).
  • Updated flyout manager state/layout mode logic to use a shared “reference width” and added stories/docs/tests/changelog to cover the new behavior.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/website/docs/components/containers/flyout/index.mdx Documents new container-relative positioning and child inheritance behavior
packages/eui/src/components/provider/component_defaults/component_defaults.tsx Allows setting EuiFlyout container via EuiProvider defaults
packages/eui/src/components/flyout/use_flyout_z_index.ts Refactors z-index API to use headerZindexLocation directly
packages/eui/src/components/flyout/use_flyout_z_index.test.ts Updates tests to match new z-index hook API
packages/eui/src/components/flyout/use_flyout_resizable.ts Adds reference-width-based percent sizing and sibling-aware clamping
packages/eui/src/components/flyout/use_flyout_resizable.test.ts Updates/adds unit tests for percent sizing and clamp logic
packages/eui/src/components/flyout/manager/types.ts Extends manager state for minWidth, containerElement, and referenceWidth
packages/eui/src/components/flyout/manager/store.ts Wires new manager actions/APIs (minWidth, setContainerElement)
packages/eui/src/components/flyout/manager/selectors.ts Adds selector for managed flyout minWidth
packages/eui/src/components/flyout/manager/selectors.test.tsx Updates selector test mocks for new manager API
packages/eui/src/components/flyout/manager/reducer.ts Stores minWidth, container element, and reference width in reducer state
packages/eui/src/components/flyout/manager/layout_mode.ts Computes layout mode using container/viewport “reference width” and stores it in state
packages/eui/src/components/flyout/manager/layout_mode.test.tsx Updates tests to match new layout-mode computation approach
packages/eui/src/components/flyout/manager/flyout_managed.tsx Passes minWidth into manager state and derives manager-dependent values from single state read
packages/eui/src/components/flyout/manager/flyout_managed.test.tsx Updates test mock to strip new non-DOM props
packages/eui/src/components/flyout/manager/flyout_containers.stories.tsx Adds Storybook demo for app-scoped vs global flyouts and resizing scenarios
packages/eui/src/components/flyout/manager/flyout_child.tsx Uses CSS var fallback to reduce position lag during resize (side-by-side)
packages/eui/src/components/flyout/manager/activity_stage.ts Refactors activity-stage logic to derive values from a single manager state read
packages/eui/src/components/flyout/manager/activity_stage.test.tsx Updates tests to match refactored activity-stage implementation
packages/eui/src/components/flyout/manager/actions.ts Adds actions for container element and reference width; extends addFlyout with minWidth
packages/eui/src/components/flyout/manager/mocks/index.ts Extends mocks with setContainerElement
packages/eui/src/components/flyout/flyout_resizable.spec.tsx Updates Cypress expectations for 90% reference-width clamping
packages/eui/src/components/flyout/flyout.styles.ts Converts named sizing from vw to % and updates fill sizing math to use CSS vars
packages/eui/src/components/flyout/flyout.styles.test.tsx Updates style tests to expect %-based sizing and updated calc strings
packages/eui/src/components/flyout/flyout.component.tsx Implements container resolution, container-rect positioning, reference-width wiring, and deprecation warnings
packages/eui/src/components/flyout/_flyout_overlay.tsx Adds headerZindexLocation plumbing to overlay mask
packages/eui/changelogs/upcoming/9377.md Adds changelog entry describing new container prop, deprecations, and resizing fixes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

packages/eui/src/components/flyout/flyout.component.tsx:601

  • The previousPadding value captured at line 559 is only set once when the effect runs, but the cleanup function uses it to restore padding. If the effect re-runs due to dependency changes (e.g., container changes), the cleanup will restore the wrong previousPadding value from the previous effect run, not the original padding before any flyout was opened. The capture should happen outside the effect or tracked in a ref to ensure the original value is preserved across effect re-runs.
      // Capture pre-existing inline padding so it can be restored on cleanup
      const previousPadding = paddingTarget.style[paddingSide];

      const paddingWidth =
        layoutMode === LAYOUT_MODE_STACKED &&
        isMainFlyout &&
        _siblingFlyoutWidth
          ? _siblingFlyoutWidth
          : width;

      if (shouldApplyPadding) {
        paddingTarget.style[paddingSide] = `${paddingWidth}px`;

        if (shouldSetGlobalPushVars) {
          setGlobalCSSVariables({
            [cssVarName]: `${paddingWidth}px`,
          });
        }
        if (isInManagedContext && flyoutManagerRef.current) {
          flyoutManagerRef.current.setPushPadding(managerSide, paddingWidth);
        }
      } else {
        paddingTarget.style[paddingSide] = previousPadding;
        if (shouldSetGlobalPushVars) {
          setGlobalCSSVariables({
            [cssVarName]: null,
          });
        }
        if (isInManagedContext && flyoutManagerRef.current) {
          flyoutManagerRef.current.setPushPadding(managerSide, 0);
        }
      }

      return () => {
        paddingTarget.style[paddingSide] = previousPadding;
        if (shouldSetGlobalPushVars) {
          setGlobalCSSVariables({
            [cssVarName]: null,
          });
        }
        if (isInManagedContext && flyoutManagerRef.current) {
          flyoutManagerRef.current.setPushPadding(managerSide, 0);
        }
      };

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +508 to +522
useLayoutEffect(() => {
if (!isMainFlyout) return;

// Only set when we have a computed percentage (during active resize)
if (typeof size === 'string' && size.endsWith('%')) {
document.documentElement.style.setProperty(
'--euiFlyoutMainWidth',
size
);
}

return () => {
document.documentElement.style.removeProperty('--euiFlyoutMainWidth');
};
}, [isMainFlyout, size]);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS custom property --euiFlyoutMainWidth is set on document.documentElement but only cleaned up when the component unmounts or when isMainFlyout or size changes. If a main flyout transitions to a non-percentage size (e.g., from resize to a named size), the CSS variable may persist with a stale percentage value. This could cause child flyouts to use the wrong offset.

Consider also removing the CSS variable when size is not a percentage string to ensure it's only present when actively needed during resize.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When size transitions from a percentage (e.g. '50%') to a named size (e.g. 'm'), React re-runs the effect. The cleanup from the previous execution fires first - and therefore clears any stale value. The CSS variable will not be set at all if the size is set to a named value because of the size.endsWith('%') check

Comment on lines +45 to +54
// Use container width when provided. When referenceWidth is 0 (e.g. container
// not yet measured by ResizeObserver), do not fall back to viewport — that
// would allow resizing beyond the container (e.g. over a sidebar). Use 0 so
// the clamp keeps the flyout at minWidth until the real width is available.
const _referenceWidth =
referenceWidth !== undefined
? referenceWidth
: typeof window !== 'undefined'
? window.innerWidth
: Infinity;
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When referenceWidth is 0 (e.g., container not yet measured), the hook uses 0 to prevent resizing beyond the container. However, this means getFlyoutMinMaxWidth will clamp to minWidth even if the user tries to resize. If minWidth is 0 or very small, the flyout could appear to "snap" to a tiny size until the container is measured. Consider showing a loading state or deferring the resize functionality until referenceWidth is available and greater than zero.

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +252
const resolveContainer = (
raw: HTMLElement | string | (() => HTMLElement | null) | null | undefined
): HTMLElement | null => {
if (raw == null) return null;
if (typeof raw === 'string') {
if (typeof document === 'undefined') return null;
const el = document.querySelector(raw);
return el instanceof HTMLElement ? el : null;
}
if (typeof raw === 'function') return raw();
if (typeof HTMLElement === 'undefined') return null;
return raw instanceof HTMLElement ? raw : null;
};
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveContainer function at line 240 does not handle the case where typeof HTMLElement === 'undefined' before checking raw instanceof HTMLElement. In server-side rendering (SSR) or environments without a DOM, HTMLElement may be undefined, and the instanceof check will throw a ReferenceError. The check at line 250 should occur before line 241 to prevent this error.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveContainer function at line 240 does not handle the case where typeof HTMLElement === 'undefined' before checking raw instanceof HTMLElement. ...

That case is checked on line 250

Comment on lines 817 to 828
@@ -511,9 +822,13 @@ export const EuiFlyoutComponent = forwardRef(
siblingFlyoutWidth,
maxWidth,
flyoutZIndex,
containerRect,
side,
isChildFlyout,
]);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inlineStyles useMemo at line 689 has layoutMode in its dependency array (line 820), but layoutMode is derived from managerState?.layoutMode which is not directly in the dependencies. If managerState changes but layoutMode stays the same, the memo won't recalculate. However, since siblingFlyoutWidth is also derived from managerState and is in the dependencies, this will likely trigger recalculation anyway. Consider adding managerState?.layoutMode directly to dependencies for clarity and to ensure the memo properly updates when the manager state changes.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding managerState?.layoutMode alongside layoutMode would be redundant. They're literally the same value.

@tkajtoch tkajtoch self-requested a review February 23, 2026 16:33
title: string;
level: EuiFlyoutLevel;
size?: string;
minWidth?: number;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for when a flyout resizes next to a fill-size sibling: the resize hook needs the sibling's minWidth to compute the clamp limit.

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

Copy link
Member

@tkajtoch tkajtoch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look great, I'm currently validating everything in Kibana and will approve when finished.

I'm seeing the implementation slowly getting too complicated for my liking. We should definitely consider refactoring in the future to better isolate logic into hooks

@tsullivan
Copy link
Member Author

Code changes look great, I'm currently validating everything in Kibana and will approve when finished.

Thanks @tkajtoch! I'm looking forward to getting this merged and moving on to the next changes, which I see as having a smaller surface area than this one.

I'm seeing the implementation slowly getting too complicated for my liking. We should definitely consider refactoring in the future to better isolate logic into hooks

I'd be happy to sync up and jam with you on this. I suggest we pull in @angeles-mb too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1️⃣ [Flyout System] resizable flyouts cause strange stacking effects

4 participants