Skip to content

[POC][Breakpoints] Resolve breakpoints against the nearest breakpoint container - #10192

Draft
Dosant wants to merge 4 commits into
elastic:mainfrom
Dosant:d/2026-09-25-surface-breakpoints-poc
Draft

Dosant wants to merge 4 commits into
elastic:mainfrom
Dosant:d/2026-09-25-surface-breakpoints-poc

Conversation

@Dosant

@Dosant Dosant commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of elastic/kibana-team#3040
Kibana side: elastic/kibana#296462

Summary

Proof of concept, not for merge.

EUI breakpoints always resolve against the window, in CSS (@media) and in JS (window resize listener). In Kibana, the sidebar narrows the app area without changing the window, so pages keep their desktop layout in a squished app area.

This PoC adds an opt-in EuiProvider option that resolves breakpoints against the nearest breakpoint container instead. true switches only the CSS mixins. { mountElement } also switches the JS hooks, which need to know where the React root is mounted:

<EuiProvider breakpointContainer>                    // CSS only
<EuiProvider breakpointContainer={{ mountElement }}> // CSS and JS
  • Consumers mark containers with data-eui-breakpoint-container (EUI_BREAKPOINT_CONTAINER_ATTRIBUTE). When the option is set, EuiGlobalStyles makes body and every marked element a euiBreakpointContainer inline-size container. body is the fallback: a container query with no matching ancestor never matches, so without it rules outside a marked element would stop applying.
  • useEuiTheme() returns breakpointContainer. euiBreakpoint, euiMinBreakpoint and euiMaxBreakpoint read it and emit @container euiBreakpointContainer (…) instead of @media. With no width condition they keep @media only screen, because a bare @container <name> is invalid.
  • With { mountElement }, EuiProvider resolves mountElement.closest('[data-eui-breakpoint-container]') ?? body, and CurrentEuiBreakpointProvider measures that element's content box, which is the same box container queries use. So useCurrentEuiBreakpoint, EuiShowFor, EuiHideFor and the components built on them follow the same width as the CSS. All providers share one ResizeObserver.
  • With { mountElement }, EuiPortal re-resolves from the portal node. CSS follows the DOM but context follows the React tree, so a flyout opened from the app would otherwise measure the app area in JS and body in CSS. It only adds a provider when the portal lands in a different container than its parent, so tooltips and popovers stay cheap.

Without the option, nothing changes.

Open questions:

  • The option's name and shape.
  • A per-call escape hatch that forces the viewport, for full-screen overlays and body scroll lock. Not in the PoC yet.
  • Whether flyouts or other surfaces should become their own containers. Out of scope for now.

Breakpoint mixins emit @container queries against a configured container name.
The JS breakpoint provider measures the element returned by getRoot(container),
where container is the element the React root or portal is mounted in.
EuiProvider takes breakpointContainer={{ mountElement }}. Containers are marked with
data-eui-breakpoint-container and resolved with closest(); body is the fallback. EUI global
styles make body and marked elements containers. JS measures the content box with a shared
ResizeObserver, and portals add a provider only when they land in a different container.
@Dosant Dosant changed the title [POC][Breakpoints] Resolve breakpoints against a surface container [POC][Breakpoints] Resolve breakpoints against the nearest breakpoint container Oct 8, 2026
breakpointContainer: true switches only the CSS breakpoint mixins.
{ mountElement } also switches the JS hooks to the resolved container.
Containers set their own container CSS instead of a global attribute rule, so the containment is visible where it applies. body stays the global fallback.
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💔 Build Failed

Failed CI Steps

History

This branch has not been deployed

No deployments
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 participant