Blog ·
A bar narrower than its window
A header collapsed on window width while a sidebar ate a third of it, and the language that overflowed by zero pixels was the one that was actually broken.
By Provibr
Every page of our panel scrolled sideways at a window width of 768 pixels. Eight pixels too wide in English, thirty eight in Romanian. It had been that way for a while, in nine languages, on every route. Those numbers come from a sweep in two currency notations; an earlier single reading of the same header had put it at three.
The cause is a sentence that is easy to nod along to and wrong: the header collapsed its contents based on the width of the window.
The window is not the bar
From the medium breakpoint upwards the sidebar sits beside the header rather than behind a hamburger, and it costs sixteen rem. So at a window of 768 pixels, the content box of the header is 464 pixels. Not 768, and not 736 either.
The two rules that were supposed to prevent the overflow, hiding the account name and shrinking the connection pill to a dot, were both written against the small breakpoint: 640 pixels of window. That is 128 pixels earlier than the point where the sidebar appears and takes its sixteen rem away. Between those two numbers there is a band where the header believes it has plenty of room and actually has less than at any narrower width, because at 640 the sidebar was still hidden.
This is not a Tailwind problem or a breakpoint that was set one step too low. It is a category error. A media query answers "how big is the viewport". The question the header needs answered is "how much room did I get", and until container queries there was no way to ask it.
Measure what the row asks for, not what it does
The temptation at this point is to nudge the breakpoint up and look at the result. That produces a number that works on the machine of whoever nudged it.
A flex row shrinks its children to their minimum content width before anything overflows, so what you see on screen is the end symptom rather than the measurement. A row can be well past its budget and still look fine, right up until one child cannot shrink further. The number to steer on is what the row asks for when nothing is constraining it: clone it, set it to maximum content width, and read it, across every language.
Done at 768 pixels in the wider currency notation, over nine languages, the parts come out like this.
| Part | Widest measurement |
|---|---|
| Three icon buttons (alerts, tasks, feedback) | 3 × 36 = 108 px |
| Five gaps between the items | 60 px |
| The connection pill, widest language | 104.8 px |
| The same status as a dot instead | 6 px |
| The language button, widest language | 83.4 px |
| The account button without a name | 132.9 px |
| The account button with a name, at its budget | 234.6 px |
| Gap to the left hand group, plus reserve | 2 × 16 px |
From that, two thresholds fall out, and both of them are stated against the content box of the header rather than the window. Showing the account name needs 524 pixels, so it starts at 34 rem, with twenty pixels of slack. Showing the pill instead of the dot needs 622.8, so it starts at 40 rem, with seventeen. Below the name threshold the row asks for 422.3 pixels, which fits comfortably in the 464 it gets.
The pleasant part of asking a container question instead of a window question is what happens next without any further code. When the sidebar is collapsed to a rail, the same 768 pixel window leaves 656 pixels of header, and the name and the pill come back on their own. Nothing knows about the rail; the bar simply measured itself again.
The language with zero pixels of overflow
The sweep that had been catching this class of defect measures document width against viewport width. Across nine languages it reported overflow everywhere, from eight pixels to thirty eight, except in French, which reported zero.
French was not fine. The connection pill had wrapped onto a second line inside a bar that is sixty four pixels tall with its items centred, so the text was quietly sitting half outside its own box while the document stayed exactly as wide as the window. No overflow sweep can see that, because nothing overflowed.
This is the same shape as an earlier defect on our marketing site, where a single word heading in a shrunken tile painted over its neighbour and the page width never changed. Text leaving its box does not widen the document. So the check has to be the other one: the right hand edge of the text against the inner edge of the box it is supposed to be in, plus the number of line boxes it occupies. In a bar with a fixed height, more than one line box is a defect on its own.
Two ways the measurement lies to you
The measuring apparatus took two attempts, and both failures pointed the same way, towards a green result that means nothing.
A clone at maximum content width has to be measured inside the container. Ours was appended to the document body, which is outside the header entirely, so the container query never matched and the clone rendered in its narrow form: 394.8 pixels where the real row wanted 515.3. Read as a before and after, that looks exactly like a successful repair. It is the same trap as measuring in euros when the customer sees dollars, or measuring a page whose data never loaded: the number is not wrong, it is an answer to a different question.
The second one is duller and worth knowing anyway. A syntax check on the sweep script passes over an unescaped backtick inside a template literal, so a probe can be subtly not the probe you wrote while the file parses fine.
Both are why the sweep grew a proof mode that hangs deliberately broken elements into the header, a block too wide for the bar, a button too narrow for its label and a line of text that runs outside its own box, and requires the probe to report each of them. It also asserts that the account button is present at all. A scanner that finds nothing is indistinguishable from a scanner that is not looking, and the only way to tell them apart is to give it something to find.
Against the unrepaired code the sweep produced seventy four failures out of ninety eight checks. Against the repair, in both currencies, 2033 out of 2033.
Truncating without breaking the button
Three smaller decisions in the repair are the kind that get reversed by someone tidying up later, so they are written down where the classes are.
The truncation lives on a span inside the button, not on the button. Putting overflow: hidden on the button itself clips its own click area and its focus ring, which turns a cosmetic fix into a keyboard accessibility defect.
The accessible name stays complete. Truncating is a presentation choice; a screen reader should still get the whole organisation name, so the label carries it in full.
And exactly one thing in that row is allowed to shrink. Without pinning the icon buttons, flex distributes the shrinking across all of them, and you get three squeezed icon buttons instead of one name that truncates cleanly. Shrinking is a decision about which element gives way, and if you do not make it, the layout engine makes it for you, evenly, which is almost never what anyone wants.
One more thing to know before reaching for this: a container also creates a layout containment boundary, which makes it the containing block for anything positioned fixed underneath it. Here that changes nothing, because the menus and drawers involved are native dialogs in the top layer, and because the bar already carried a backdrop filter, which has the same effect. That was checked by opening them rather than reasoned about, and if the answer had been different, the container would have had to go somewhere else.
Which raises the last constraint: the container has to be declared in the one function that builds every header, because the admin header mounts the same account menu. A container query with no container never matches, silently. Declare it in one place and forget the other, and the account name disappears from an entire section of the product with nothing to show for it in any log.
What we would write down
- A media query answers a question about the viewport. If a component shares its width with a sidebar, that is the wrong question, and the answer is wrong by the exact width of the sidebar.
- Steer on what a row asks for, not on what it does. Flex shrinks children first, so the screen shows you the end symptom.
- Measure the clone inside the container. Outside it, the query does not match and you measure the narrow shape, which reads as a fix.
- Zero pixels of overflow can be the broken case. Text leaving its box does not widen the document; check the text against its own box and count line boxes.
- Give the scanner something to find. A proof mode that plants known defects is the only way to tell "nothing wrong" from "not looking".
- Truncate on an inner element. Clipping the button clips the focus ring, and the accessible name should stay whole.
- Choose which element shrinks. Left alone, flex shares the pain evenly across everything.
For clarity about what this is: the repair is built, measured across nine widths, nine languages, five routes and both currencies, and running. It has not been measured on a production build, so nothing here says anything about layout shift, and one organisation name was used throughout.
From the platform work on Provibr. The page whose overflow started this is in offline next to a heading that says online, and what nine languages do to a set of fixed sized controls is in sixteen thousand keys and nine languages.