mui / mui/base-ui

[dialog] Scroll lock is skipped when the page scroller is <body> under an overflow:hidden <html>

Open
#5,720 0 comments 0 reactions 0 assignees View on GitHub
component: dialog status: waiting for maintainer
Dominant language
TypeScript
Stars
10.9k
Forks
543
Avg merge
1d 20h
Merged PRs (30d)
101

Description

## Current behavior

When a third party moves the page scroll container onto `` by locking ``, Base UI never applies a scroll lock, and the page stays scrollable behind an open modal `Dialog`.

The setup that triggers it:

```css
html { height: 100dvh; overflow: hidden; }
body { height: 100%; overflow-y: auto; }
```

`useScrollLock` reads ``, sees `overflow: hidden`, and treats the page as already locked:

```js
// If the site author already hid overflow on , respect it and bail out.
if (htmlOverflowY === 'hidden' || htmlOverflowY === 'clip') { this.restore = NOOP; return; }
```

With an open `Dialog`, `` and `` carry no inline styles, `data-base-ui-scroll-locked` is absent, and `getComputedStyle(document.body).overflowY` is still `auto` — the page scrollbar remains visible and can be dragged with the mouse.

This is not the same as #4640 / #4665. That covers a *transient* lock from another overlay that later clears. Here nothing ever clears: `` is legitimately and permanently locked, and `` is an independent scroll container.

I also ported the current `@base-ui/utils@0.4.0` logic verbatim and ran it against the same page — it still does not lock:

```js
function getViewportScroller(html, body) {
return isOverflowElement(html) ? html : body;
}
```

`isOverflowElement(html)` is `true` here, so it returns ``, reads `hidden`, and concludes the page is locked. The #4665 `MutationObserver` handoff then waits for a lock that never clears, so the dialog never takes over and an observer is left attached on every open.

## Expected behavior

`` is the element that actually scrolls, so it should be locked while a modal `Dialog` is open.

## Reproducible example

Any modal `Dialog` on a page with the stylesheet below. Verified against a minimal app:

```html

html { height: 100dvh; overflow: hidden; }
body { height: 100%; overflow-y: auto; margin: 0; }

```

```jsx
function App() {
const [open, setOpen] = React.useState(false);
return (
<>
setOpen(true)}>open
{Array.from({ length: 60 }, (_, i) =>

Page paragraph {i}

)}




Title
setOpen(false)}>close




);
}
```

Open the dialog, then drag the page scrollbar with the mouse — the page behind scrolls. Inspect ``/``: no inline styles were written.

## Base UI version

`@base-ui/react@1.3.0` (`@base-ui/utils@0.2.6`). Detection logic from `1.8.0` / `@base-ui/utils@0.4.0` verified to behave the same way.

## Which browser are you using?

Chrome

## Which OS are you using?

Linux (also reproduces on the same markup in Chrome on other platforms)

## Additional context

This is not a contrived setup. We hit it on a production marketing site: the Qualified chat widget's "docking" feature (`js.qualified.com/docking.js`) injects, on every page at load and regardless of whether the panel is ever docked:

```css
html { height: 100dvh !important; overflow: hidden !important; contain: inline-size layout !important; }
body { overflow: hidden auto !important; height: 100% !important; width: calc(100% + 15px) !important; }
```

It does this so it can shrink the page for a docked side panel without layout shift. The same "`` as a fixed frame, `` as the scroller" pattern is common in app shells generally.

Two things that might be worth considering:

1. **Detection.** Before concluding the page is locked, it may be worth checking whether the *other* element is itself a scroll container — e.g. `` is `hidden`/`clip` but `` has `overflow-y: auto|scroll` and `scrollHeight > clientHeight`. In that case `` is the page scroller and is what needs locking.

2. **Application.** Inline styles lose to author `!important`. In the case above, `body { overflow: hidden auto !important }` cannot be overridden by `body.style.overflow = 'hidden'`, so even correct detection would not be enough. `setProperty('overflow', 'hidden', 'important')` or an injected stylesheet rule would be robust against this. (MUI's `ModalManager` has the same limitation for the same reason.)

Contributor guide

Open the contributing guide

Research direction

Start by tracing the useScrollLock path through getViewportScroller and isOverflowElement, then reproduce the issue with the html/body CSS and modal Dialog example provided. Done means the independently scrolling body is locked while the Dialog is open, the page cannot be dragged behind it, styles are restored on close, and no observer remains attached.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.