TL;DR: A banner under the navigation measured 549px wide inside a 375px phone viewport. Its title had
flex-shrink: 0, so it stayed 399px wide, squeezed the value column to zero and pushed the main call-to-action tox=452— past the right edge. Because the page usedoverflow-x: hidden, there was no scrollbar: the button was simply gone. The fix was a mobile-only grid withminmax(0, 1fr), a tap-target for the close button, and a sticky action row for the expanded panel.
The scenario
A one-line status banner sits directly under the navigation. It says a few things about the current session in compact, monospace chips and offers a "View all data" call-to-action that expands a panel with the full breakdown. On desktop it is one tidy row. It was reviewed, it passed, and nobody had reason to doubt it.
Then I resized the window to the phone width where most visitors actually arrive and the row quietly fell apart. Not with an error, not with a scrollbar — with a missing button. When a control disappears without breaking the build, it survives every review that only looks at a desktop screenshot.
The measurements below come from driving a real headless Chrome over the DevTools protocol and reading the geometry of the live DOM. There is no substitute for measuring the elements you actually shipped.
The pattern
A nowrap, non-shrinkable title guarantees overflow
The title element carried two perfectly reasonable-looking declarations:
.barTitle { flex-shrink: 0; white-space: nowrap; }
Each is fine on its own. Together, inside a flex row, they are a trap: the title refuses to shrink and refuses to wrap, so on a narrow screen it stays wider than the space it was given. Everything after it gets what is left — which is nothing.
A flex column should have been minmax(0, 1fr)
The value column was flex: 1. Flex items have an automatic minimum size, so a flex: 1 item will not shrink below its content width unless you tell it to. The title consumed the row, the value column collapsed to 0px, and the pinned action on the right was pushed past the edge. The mobile-friendly fix is to let the middle track actually shrink — with grid, minmax(0, 1fr).
overflow-x: hidden turned off-screen into gone
The global stylesheet guards against horizontal scroll with overflow-x: hidden. That guard is exactly what hides this class of bug: body.scrollWidth reported 375, so nothing looked wrong to an automated check, while the action's getBoundingClientRect().right was 549. There was no scrollbar to reveal it and no console warning to report it. A control that exists, is focusable and still cannot be reached is not a cosmetic issue — especially when it is a privacy control.
Mobile bugs come in pairs
Fixing the collapsed bar exposed a second, quieter failure: the expanded panel. Its action buttons — the ones that let a user dismiss or collapse the panel — sat roughly 2000px below the fold, at the very bottom of a long scroll. On a phone, "the buttons work" and "the buttons are reachable" are two different claims.
The measure, before and after
| Viewport | Before | After |
|---|---|---|
| 375px | scrollWidth 549; action right edge 549 (off-screen); value column 0px wide; close target 24×24 | scrollWidth 375; action right edge 329; value column 221px; close target 32×32; title wraps to two lines |
| 768px | — | Bar height 66px, every control inside the viewport |
| 1280px | Single row, height 37px | Single row, height 37px — desktop pixel-identical |
The fix, in full, is a mobile-only layout:
@media (max-width: 855px) {
.barMain {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
flex-basis: calc(100% - 2.75rem);
}
.dot { grid-area: 1 / 1; }
.barTitle { grid-area: 1 / 2 / 2 / 4; white-space: normal; min-width: 0; }
.barValues { grid-area: 2 / 1 / 3 / 3; }
.barAction { grid-area: 2 / 3; justify-self: end; }
.barClose { width: 32px; height: 32px; }
.actions { position: sticky; bottom: 0; }
}
The attack path of a disappearing control
Reproducing it in the lab
Every probe targets a page I control locally. The point is not the banner — it is the measurement technique.
# 1. read the geometry of the control you actually shipped, per viewport
node cdp-measure.js --viewport 375x812
# <- dump: viewport, body.scrollWidth, and each element's rect
# 2. the assertion that matters: nothing may exceed the viewport width
node -e "const r=require('./rects.json'); \
const over=r.filter(e=>e.right>r.viewport); \
if(over.length){console.error('OFFSCREEN',over);process.exit(1)}"
# <- fails loudly when a control sits past the right edge
# 3. check that the guard is not hiding the problem
node -e "const d=await page.evaluate(()=>({ \
scrollW: document.scrollingElement.scrollWidth, \
overflowX: getComputedStyle(document.documentElement).overflowX }));"
# <- scrollW equals viewport *because* overflow-x is hidden, not because it fits
# the measurement is the test:
# for each breakpoint (320, 375, 768, 1024, 1280):
# every interactive element's rect must be inside 0 .. viewportWidth
# the action row must be reachable without scrolling past the fold
# tap targets must be >= 44px in the narrowest layout
Detecting it in your own site
- Measure, never eyeball. A desktop screenshot proves nothing about the layout that renders at 375px. Drive a real engine and read
getBoundingClientRect(). - Assert against the viewport, not against a screenshot. Fail the check when any interactive element's
rightexceeds the viewport width, so overflow cannot pass review again. - Do not trust
overflow-x: hidden. It hides the symptom. IfscrollWidthequals the viewport only because overflow is clipped, you are not safe — you are blind. - Treat wrapping as a feature.
flex-shrink: 0pluswhite-space: nowrapis a promise you cannot keep on small screens. Reach forminmax(0, 1fr)and let the tagline wrap. - Test the expanded state, not just the collapsed one. Long panels hide their action buttons at the bottom of a scroll; a sticky action row keeps them reachable.
- Fix the tap targets while you are in there. A 24×24 close button under the 44px recommendation is a second bug living in the same component.
Remediation
# immediately
1. give the mobile bar a grid: auto | minmax(0, 1fr) | auto
2. allow the title to wrap: white-space normal; min-width 0
3. keep the action in the layout: justify-self end, never margin-left auto
4. raise the close button to a 32px+ tap target; 44px where space allows
# within the week
5. make the expanded panel actions sticky at the bottom of the viewport
6. re-measure 320 / 375 / 768 / 1024 / 1280 and assert nothing exceeds the width
7. add the assertion to CI so the next template change cannot reopen it
# within the month
8. audit every other fixed-width, nowrap chip row in the design system
9. verify that privacy and consent controls are reachable in the narrowest layout
The cost
This is not a cosmetic find. The control that left the screen was the entry point to the banner's detail view — the place a visitor goes to see and act on what was collected. A consent or data control the user cannot reach is a broken control, and a broken control on the exact page that promises transparency is the worst place for it.
- The affected users are the ones on the smallest screens — mobile is the majority of traffic, not an edge case.
- The failure is silent: no error, no scrollbar, no console warning. It hides from reviews that trust a desktop render.
- It is a class of bug, not a one-off. Any
flex-shrink: 0+nowrapchip row in the same design system has the same latent failure.
Takeaways
- Measure the layout you shipped, at the widths your visitors use. A screenshot at one size is an assumption, not evidence.
flex-shrink: 0andwhite-space: nowrapare fine until the screen is narrow. On mobile they are how titles eat buttons.overflow-x: hiddendoes not fix overflow; it hides the proof. Assert element rectangles against the viewport instead.- If a control is a privacy or consent control, "reachable on mobile" is part of its definition, not a nice-to-have.
- Fix the breakpoint, then guard it. The same row will come back the next time someone edits the template.