~/blog/banner-left-the-screenPOSTED
by Shady Nathan Tawfik · October 9, 2026 · 7 min

The Banner That Left the Screen: A Mobile Layout Postmortem

A privacy banner looked perfect on desktop and passed review. At 375px its primary call-to-action sat at x=452 in a 375px viewport — off-screen and untappable. The title refused to shrink, the value column collapsed to zero, and a global overflow guard hid the evidence. This is how a three-column grid fixed it.

Interactive[blog-2.0]

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 to x=452 — past the right edge. Because the page used overflow-x: hidden, there was no scrollbar: the button was simply gone. The fix was a mobile-only grid with minmax(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:

~/code/css css
.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.

TL;DR: A privacy banner looked perfect on desktop and passed review. At 375px its main call-to-action sat at x=452 in a 375px viewport — off-screen and untappable. The title refused to shrink, the value column collapsed to zero, and an overflow guard hid the evidence. The fix was a three-column grid.

The measure, before and after

ViewportBeforeAfter
375pxscrollWidth 549; action right edge 549 (off-screen); value column 0px wide; close target 24×24scrollWidth 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
1280pxSingle row, height 37pxSingle row, height 37px — desktop pixel-identical

The fix, in full, is a mobile-only layout:

~/code/css css
@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

Desktop review passesTitle has flex-shrink 0 and nowrapAt 375px the title stays 399px wideMiddle column collapses to 0pxAction pushed to right edge 549overflow-x hidden removes the scrollbarCTA and consent controls become unreachable

Reproducing it in the lab

Every probe targets a page I control locally. The point is not the banner — it is the measurement technique.

~/code/bash bash
# 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
~/code/text text
# 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 right exceeds the viewport width, so overflow cannot pass review again.
  • Do not trust overflow-x: hidden. It hides the symptom. If scrollWidth equals the viewport only because overflow is clipped, you are not safe — you are blind.
  • Treat wrapping as a feature. flex-shrink: 0 plus white-space: nowrap is a promise you cannot keep on small screens. Reach for minmax(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

~/code/text text
# 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 + nowrap chip 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: 0 and white-space: nowrap are fine until the screen is narrow. On mobile they are how titles eat buttons.
  • overflow-x: hidden does 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.
Shady Nathan Tawfik

Founder & Cyber IT Security Engineer in Bielefeld. Offensive security, audits and full-stack builds. More at

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026