@@ -31,15 +31,15 @@ or completely invisible.
3131which triggers the browser's native ` scrollIntoView ` . So after opening, the page automatically
3232scrolls to bring the dialog into view — scrolling the user away from where they were.
3333
34- ### The fix: wrap the app content in a scroll container
34+ ### The fix: clip the horizontal overflow in a wrapper
3535
3636The root cause is the document overflowing horizontally. The fix is to ** never let the
37- document itself overflow in X** — instead, contain horizontal scroll inside a child wrapper.
37+ document itself overflow in X** — instead, contain the overflow inside a child wrapper.
3838
3939``` html
4040<body >
41- <!-- This wrapper is the scroll container for the whole app -->
42- <div style =" overflow-x : auto ;" >
41+ <!-- This wrapper absorbs the horizontal overflow of the whole app -->
42+ <div style =" overflow-x : clip ;" >
4343 <!-- all app content goes here -->
4444 </div >
4545
@@ -54,20 +54,74 @@ With this structure:
5454- ` position: fixed; margin: auto ` centers the dialog correctly
5555- No ghost empty space at the bottom
5656
57- As a safety net, add ` overflow-x: hidden ` on ` html ` and ` body ` to prevent any content
57+ As a safety net, add ` overflow-x: clip ` on ` html ` and ` body ` to prevent any content
5858that forgets to use a wrapper from inflating the layout viewport:
5959
6060``` css
6161html ,
6262body {
63- overflow-x : hidden ;
63+ overflow-x : clip ;
6464}
6565```
6666
67- Note: ` overflow: hidden ` on ` <html> ` does create a new containing block, which could
68- break ` position: fixed ` in edge cases — but in practice this safety net is worth having,
69- and any element that needs correct ` position: fixed ` behavior (like ` <dialog> ` ) should
70- be moved to ` document.body ` directly anyway (which ` dialog.jsx ` already does).
67+ What it costs: content wider than the screen is cut off instead of reachable by dragging.
68+ When some element genuinely needs to be scrolled horizontally (a wide table, a carousel),
69+ give ** that element** its own ` overflow-x: auto ` — the wrapper stays ` clip ` .
70+
71+ ### Why ` clip ` and not ` auto ` or ` hidden `
72+
73+ ` clip ` is the only value that clips without turning the box into a ** scroll container** .
74+ ` auto ` , ` scroll ` and ` hidden ` all create one, and that has two consequences that show up
75+ far from the wrapper:
76+
77+ - ** Every ` position: sticky ` in the app sticks to that wrapper** , because sticky resolves
78+ against the nearest scroll container in the DOM — not against the box the app considers
79+ its scroller. The wrapper grows with its content and never scrolls, so nothing sticks
80+ anymore: sticky headers and ` <List groupBy> ` group labels just scroll away with the
81+ content.
82+ - ** Worse than not sticking: the sticky element is offset downwards.** The rectangle a
83+ sticky element sticks within is the scroll container's box shrunk by its
84+ ` scroll-padding ` (CSS Position L3), and Chromium applies that for an element scroll
85+ container. A wrapper carrying ` data-navi-fixed-bar-space ` has
86+ ` scroll-padding-top: var(--navi-fixed-bar-space-top) ` , so labels come to rest at
87+ ` scroll-padding-top + top ` — a group label floating a hundred pixels below the bar,
88+ covering the content above it.
89+
90+ ` overflow-x: clip ` also lets ` overflow-y ` stay ` visible ` , where ` hidden ` /` auto ` force the
91+ other axis to ` auto ` .
92+
93+ Note: ` overflow: hidden ` on ` <html> ` would additionally create a new containing block,
94+ which could break ` position: fixed ` in edge cases. Any element that needs correct
95+ ` position: fixed ` behavior (like ` <dialog> ` ) should be moved to ` document.body ` directly
96+ anyway (which ` dialog.jsx ` already does).
97+
98+ ### The wrapper is a net, not a fix: find what overflows
99+
100+ Clipping makes the symptom disappear, and with it the signal. Something wider than
101+ the screen is a layout bug wherever it happens — a width in px, a ` min-width ` , a grid
102+ of fixed columns, an unbreakable string coming from the data. The wrapper only keeps
103+ that bug from taking the whole mobile viewport down with it.
104+
105+ So in dev, ask who overflows:
106+
107+ ``` js
108+ import { detectHorizontalOverflow } from " @jsenv/navi" ;
109+
110+ if (import .meta.dev) {
111+ detectHorizontalOverflow ({ root : document .querySelector ("#main ") });
112+ }
113+ ` ` `
114+
115+ It outlines the culprits in red and names them in the console, at load and whenever
116+ the layout changes. It reports the **outermost** box that sticks out (its children
117+ stick out because it does), and stays quiet about what cannot reach the document:
118+ anything inside a box that scrolls or clips on its own — a wide table in its own
119+ ` overflow- x: auto` container is doing the right thing — and anything ` position: fixed`
120+ or in the top layer.
121+
122+ Measuring against the wrapper matters here: once it is in ` clip` there is no scrollable
123+ overflow left to read, so ` scrollWidth > clientWidth` reports nothing. The rectangles of
124+ the descendants are what tells.
71125
72126### Also: place ` < dialog> ` before content in the DOM
73127
@@ -83,7 +137,8 @@ before opening for this reason.
83137
84138### Summary
85139
86- | Cause | Symptom | Fix |
87- | ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
88- | Document overflows horizontally | Layout viewport inflated → ghost space below → dialog miscentered | Wrap app content in ` overflow-x: auto ` container |
89- | ` <dialog> ` at end of DOM | Page scrolls to dialog on ` showModal() ` | Place ` <dialog> ` first in ` <body> ` |
140+ | Cause | Symptom | Fix |
141+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
142+ | Document overflows horizontally | Layout viewport inflated → ghost space below → dialog miscentered | Wrap app content in ` overflow- x: clip` container |
143+ | ` < dialog> ` at end of DOM | Page scrolls to dialog on ` showModal ()` | Place ` < dialog> ` first in ` < body> ` |
144+ | Wrapper uses ` auto` /` hidden` | It becomes a scroll container: sticky headers and group labels stick to it (and get offset by its ` scroll- padding` ) | Use ` clip` ; put ` overflow- x: auto` on the wide element itself |
0 commit comments