Every page scrolls sideways on a phone, and one 651-pixel row is the whole reason #113

Closed
opened 2026-09-07 18:54:29 +00:00 by tiagoagueda · 1 comment
Owner

Measured, not estimated

Chromium, signed in, thirteen pages, scrollWidth - clientWidth on the document:

Viewport Overflow Pages affected
320 px 331 px 13 of 13
400 px 251 px 13 of 13

WCAG 2.2 SC 1.4.10 Reflow is level AA and asks for no horizontal scrolling at 320 CSS
pixels. Postulo fails it on every page it has.

It is one element

The overflow is identical everywhere, which is the tell: 331 + 320 = 651, and
251 + 400 = 651. Something is 651 pixels wide and does not care about the viewport. It is
the main navigation in base.html:

DIV.flex items-center gap-1   width 651   right edge 651
  A.nav-link "Board"      right 357
  A.nav-link "Documents"  right 457
  A.nav-link "Companies"  right 556
  A.nav-link "Reminders"  right 651

A flex row that does not wrap and has no narrow-screen behaviour. Page content is not the
problem -- #91 fixed the one table that was -- and no amount of fixing pages will help while
the header is 651 pixels wide.

Why the tests did not catch it

The axe suite visits every page and passes, and that is not axe failing: reflow is not
something axe checks
, because it is a question about layout at a width rather than about
the document. The suite also runs at the default viewport, so nothing has ever looked at
these pages narrow.

Comparing scrollWidth with clientWidth at 320 px is three lines and fails today on all
thirteen. That check is worth more than the fix, because it is what stops the next component
doing this.

Its relation to #73

#73 -- Readable and usable on a phone -- is the same territory, sits on 0.6.0 and is tier
4. This is filed separately and higher on purpose. #73 is about a phone being pleasant; this
is a level AA conformance failure on every page, against a README that says "Keyboard,
screen readers, scripts off, both themes at WCAG 2.2 AA" without qualification. Whether they
merge is the maintainer's call, but the promise and the polish are not the same commitment.

Classification

Accessibility, bug. Not solved here, as asked.

## Measured, not estimated Chromium, signed in, thirteen pages, `scrollWidth - clientWidth` on the document: | Viewport | Overflow | Pages affected | | --- | --- | --- | | 320 px | **331 px** | 13 of 13 | | 400 px | **251 px** | 13 of 13 | **WCAG 2.2 SC 1.4.10 Reflow is level AA** and asks for no horizontal scrolling at 320 CSS pixels. Postulo fails it on every page it has. ## It is one element The overflow is *identical* everywhere, which is the tell: 331 + 320 = 651, and 251 + 400 = 651. Something is 651 pixels wide and does not care about the viewport. It is the main navigation in `base.html`: ``` DIV.flex items-center gap-1 width 651 right edge 651 A.nav-link "Board" right 357 A.nav-link "Documents" right 457 A.nav-link "Companies" right 556 A.nav-link "Reminders" right 651 ``` A flex row that does not wrap and has no narrow-screen behaviour. Page content is not the problem -- #91 fixed the one table that was -- and no amount of fixing pages will help while the header is 651 pixels wide. ## Why the tests did not catch it The axe suite visits every page and passes, and that is not axe failing: **reflow is not something axe checks**, because it is a question about layout at a width rather than about the document. The suite also runs at the default viewport, so nothing has ever looked at these pages narrow. Comparing `scrollWidth` with `clientWidth` at 320 px is three lines and fails today on all thirteen. That check is worth more than the fix, because it is what stops the next component doing this. ## Its relation to #73 #73 -- *Readable and usable on a phone* -- is the same territory, sits on 0.6.0 and is tier 4. This is filed separately and higher on purpose. #73 is about a phone being pleasant; this is a **level AA conformance failure on every page**, against a README that says "Keyboard, screen readers, scripts off, both themes at WCAG 2.2 AA" without qualification. Whether they merge is the maintainer's call, but the promise and the polish are not the same commitment. ## Classification Accessibility, bug. Not solved here, as asked.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 18:54:29 +00:00
Author
Owner

Fixed in 5eda1a4. The diagnosis in the report was right and the arithmetic held: nav.flex measured 635 pixels, and it was on every page.

The fix. Below 768 pixels the row is a disclosure — the same <details> the account menu beside it has always used, so it opens with no script, closes with Escape, and the links are written once and included twice with only one copy ever in the layout. Wrapping the row was the other candidate and would have been one class; it would also have put three lines of navigation above every page on a phone.

Five more, all of which the navigation was hiding. Once 331 pixels of overflow came off every page, what was underneath became visible:

Page Over Cause
/server/overview/ 223 truncate sets white-space: nowrap, so a database path's smallest possible width is its whole width. A flex item will not go under that, so the row widened the card, the card widened the grid track, the track widened the page.
/listings/ 144 See below.
/server/defaults/ 55 Same .sr-only cause as /listings/.
/capture-tokens/ 44 The openapi.json address. No word boundary, so no browser will break it.
/jobs/companies/ 12 Three buttons in a row that would not wrap.
/server/people/<id>/plugins/ 1 A w-72 shrink-0 column — 288 pixels inside a card inside a 320-pixel page.

The one worth writing down. /listings/ scrolled 144 pixels because of <span class="sr-only">Actions</span> — one pixel wide, invisible to everybody, sitting in the last header cell of a 645-pixel table that had its own overflow-x: auto box. .sr-only is position: absolute, and an absolutely positioned box is confined by its containing block, not by an ancestor's overflow. The scroll wrapper was position: static, so the span was positioned against the viewport, stepped straight out of the scroll box, and took the document with it.

Every box in Postulo that may be wider than the page is now .scroll-x, which is relative overflow-x-auto with the reason written above it. A scroll container that is not also a containing block is a trap, and it is not a visible one.

The test. tests/e2e/test_reflow.py, over every page the signed-in browser suite already walks — the shared path list #115 introduced, so a page added later is measured narrow as well as inspected.

It asks the browser to scroll the page and reports how far it got, rather than comparing scrollWidth to clientWidth as the report suggested. That three-line version would have been wrong in both directions here: documentElement.scrollWidth counted that same clipped sr-only box on pages that do not scroll at all, and reported 2-pixel "overflows" on /applications/1/ where window.scrollX stays 0. Asking the browser to actually scroll is the criterion's own question and has no false positives.

The failure names the element, not the page — nav.flex at 635 wide is a fix, /applications/ overflows by 331 is a search. Boxes inside something that scrolls on purpose are not reported, which is the criterion's own exemption for content that needs two dimensions; boxes that escape such a container by being absolutely positioned are.

On its relation to #73. Left open and untouched. This was the conformance failure; #73 is still the question of whether a phone is pleasant, and the two screenshots I took while checking this say it is not the same job — the header works, but the settings sidebar and several forms are merely legal rather than good.

One new string, Menu, translated into all 24 catalogues and flagged draft.

Fixed in 5eda1a4. The diagnosis in the report was right and the arithmetic held: `nav.flex` measured 635 pixels, and it was on every page. **The fix.** Below 768 pixels the row is a disclosure — the same `<details>` the account menu beside it has always used, so it opens with no script, closes with Escape, and the links are written once and included twice with only one copy ever in the layout. Wrapping the row was the other candidate and would have been one class; it would also have put three lines of navigation above every page on a phone. **Five more, all of which the navigation was hiding.** Once 331 pixels of overflow came off every page, what was underneath became visible: | Page | Over | Cause | |---|---|---| | `/server/overview/` | 223 | `truncate` sets `white-space: nowrap`, so a database path's *smallest possible* width is its whole width. A flex item will not go under that, so the row widened the card, the card widened the grid track, the track widened the page. | | `/listings/` | 144 | See below. | | `/server/defaults/` | 55 | Same `.sr-only` cause as `/listings/`. | | `/capture-tokens/` | 44 | The `openapi.json` address. No word boundary, so no browser will break it. | | `/jobs/companies/` | 12 | Three buttons in a row that would not wrap. | | `/server/people/<id>/plugins/` | 1 | A `w-72 shrink-0` column — 288 pixels inside a card inside a 320-pixel page. | **The one worth writing down.** `/listings/` scrolled 144 pixels because of `<span class="sr-only">Actions</span>` — one pixel wide, invisible to everybody, sitting in the last header cell of a 645-pixel table that had its own `overflow-x: auto` box. `.sr-only` is `position: absolute`, and an absolutely positioned box is confined by its *containing block*, not by an ancestor's overflow. The scroll wrapper was `position: static`, so the span was positioned against the viewport, stepped straight out of the scroll box, and took the document with it. Every box in Postulo that may be wider than the page is now `.scroll-x`, which is `relative overflow-x-auto` with the reason written above it. A scroll container that is not also a containing block is a trap, and it is not a visible one. **The test.** `tests/e2e/test_reflow.py`, over every page the signed-in browser suite already walks — the shared path list #115 introduced, so a page added later is measured narrow as well as inspected. It asks the browser to scroll the page and reports how far it got, rather than comparing `scrollWidth` to `clientWidth` as the report suggested. That three-line version would have been wrong in both directions here: `documentElement.scrollWidth` counted that same clipped `sr-only` box on pages that do not scroll at all, and reported 2-pixel "overflows" on `/applications/1/` where `window.scrollX` stays 0. Asking the browser to actually scroll is the criterion's own question and has no false positives. The failure names the element, not the page — `nav.flex` at 635 wide is a fix, `/applications/ overflows by 331` is a search. Boxes inside something that scrolls on purpose are not reported, which is the criterion's own exemption for content that needs two dimensions; boxes that escape such a container by being absolutely positioned are. **On its relation to #73.** Left open and untouched. This was the conformance failure; #73 is still the question of whether a phone is *pleasant*, and the two screenshots I took while checking this say it is not the same job — the header works, but the settings sidebar and several forms are merely legal rather than good. One new string, `Menu`, translated into all 24 catalogues and flagged `draft`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#113
No description provided.