One Dangling grid-area Shrank a Sidebar to 48px

Open your own site on a phone and the content column is squeezed into a thin ribbon, Chinese text wrapping one character per line, the page ballooning past five thousand pixels tall. On desktop everything looks fine. The responsive media query clearly says “one column on small screens”, and that block of CSS hasn’t been touched in weeks.
I checked the fonts. I checked word-break. I checked the classic flexbox min-width: auto trap. None of it. The actual culprit was one line that looks completely harmless — and that had in fact been dead for a long time:
.tag-sidebar {
grid-area: sidebar;
}
The parent .layout-grid had never defined grid-template-areas. That sidebar area simply does not exist.
Intuitively, referencing a named area that isn’t there should behave like a typo in a property name: the browser doesn’t understand it, so it skips it. That is not how CSS Grid works. It doesn’t ignore the reference. It tries its best to satisfy it — by growing new tracks out of thin air.
Start with the numbers: one line’s worth of difference
Rather than describe it, measure it. Here is a minimal reproduction: a two-column grid whose parent declares grid-template-columns: minmax(0, 1fr) 200px, collapsing to a single 1fr column under @media (max-width: 1023px). The only variable is whether the child carries that grid-area: sidebar line.
Reading getComputedStyle(layout).gridTemplateColumns with Playwright gives you the tracks the browser actually resolved, not the string you wrote into your stylesheet:
| Viewport | grid-area | Resolved tracks | Sidebar width |
|---|---|---|---|
| 1280px | absent | 1056px 200px | 200px |
| 1280px | sidebar | 960px 200px 0px 48px | 48px |
| 390px | absent | 390px | 390px |
| 390px | sidebar | 294px 0px 48px | 48px |
Two things are worth pausing on.
First, desktop went from 2 tracks to 4. You declared two; the browser handed you four, the extras being 0px and 48px. The sidebar did not land in the second track where you expected it. It was placed in the fourth, at 48px — the width of the longest unbreakable word inside it.
Second, and far less intuitive: the mobile media query is effectively dead. The CSS plainly says grid-template-columns: 1fr, yet what resolves is three tracks, 294px 0px 48px. Open DevTools and that 1fr declaration is sitting there active, not struck through, not overridden. The problem isn’t that your declaration lost a cascade fight. It’s that the browser added something alongside it.
This is exactly why staring at a screenshot gets you nowhere. You’ll chase fonts, wrapping, and flexbox, because the number of columns looks wrong while the number of columns you wrote is right. Only measuring the computed style reveals the two tracks you never wrote.
Why this happens: three layers of spec
This is not a Chromium bug. I ran the same reproduction through Chromium, Firefox, and WebKit, and all three resolve to byte-identical track listings — 960px 200px 0px 48px on desktop, 294px 0px 48px on mobile, in every engine. When three independent implementations “get it wrong” in exactly the same way, that usually means it isn’t wrong at all: it’s what the spec asks for.
It falls out of three CSS Grid rules chained together, each perfectly reasonable on its own.
Layer one: how grid-area expands. Per MDN, when grid-area is given a single <custom-ident>, all four longhands receive that value:
grid-area: sidebar;
/* is equivalent to */
grid-row-start: sidebar;
grid-column-start: sidebar;
grid-row-end: sidebar;
grid-column-end: sidebar;
Note that things are already going sideways: one shorthand becomes four independent placement declarations, two on each axis.
Layer two: the fallback when no named line matches. Each longhand first looks for a line called sidebar-start (or sidebar-end). grid-template-areas generates that pair of lines automatically for every named area — but we never defined grid-template-areas, so there is nothing to find. The spec’s handling of that case:
If there is a named line with the name
<custom-ident>-start, it contributes the first such line to the grid item’s placement. Otherwise, this is treated as if the integer1had been specified along with the<custom-ident>.
So it degrades to grid-column-start: sidebar 1 — “the first line named sidebar”.
Layer three, the decisive one: what happens when no line named sidebar exists either. The spec does not say “give up”. It says:
If a name is given as a
<custom-ident>, only lines with that name are counted. If not enough lines with that name exist, all implicit grid lines are assumed to have that name for the purpose of finding this position.
Implicit grid lines are assumed to carry the name. To give sidebar 1 something to resolve against, the browser grows implicit tracks outward until it has a line to hand you.
Stack the three together and a name pointing at nothing gets faithfully translated into “please open a few more tracks outside the explicit grid”. And because layer one expanded it into two declarations per axis, it happens on both axes at once.
Why nothing warns you
This part is worth remembering more than the bug itself.
CSS has no error class for “you referenced something that doesn’t exist”. A misspelled property (grid-are: sidebar) gets dropped at parse time. But grid-area: sidebar is syntactically flawless — <custom-ident> is a legitimate value type and sidebar is a legitimate identifier. The parser has no grounds to reject it. What that name points at is a question for the layout algorithm at layout time, and the layout algorithm’s own spec explicitly defines “fall back to implicit lines”, so it doesn’t count as a failure there either.
The result:
- Linters can’t catch it. stylelint has no way, while checking one file, to know whether some other selector — possibly in another file — defines a matching
grid-template-areas. That requires cross-selector layout reasoning, which is outside what linting does. - DevTools won’t flag it. The declaration is valid, so the Styles panel renders it normally.
- The build won’t fail. To a bundler this is just a valid run of CSS text.
- The console stays clean.
The entire toolchain is satisfied. Only the resolved layout knows better. Failures that quietly do something else are an order of magnitude harder to track down than failures that throw, because you don’t even get a hint about where to look.
How the line survived
Its provenance is worth adding, because it dictates the prevention.
The obvious guess: the layout originally used named areas, was later reworked to use grid-template-columns, the parent’s grid-template-areas was deleted, and the child’s line was forgotten. “Deleted the definition, forgot the reference” is a common enough story.
The commit history says otherwise. The parent never defined grid-template-areas — not once. That file has exactly one commit in its history, which means this grid-area: sidebar was wrong from the moment it was written. It isn’t a leftover that went stale; it never corresponded to anything at all.
That is arguably more alarming than the forgotten-deletion version. If a definition had been removed, there would at least have been a moment when the code was correct, and a diff where the inconsistency could have been spotted in review. Instead: a line of CSS that was never valid got written, passed review, passed the build, shipped to production, looked fine on desktop, and sat there quietly until someone tested a narrow viewport. Not one stage in that chain had a chance to stop it — because, as above, the entire toolchain considers the line perfectly fine.
The lesson: grid-template-areas and the children’s grid-area are one unit — use both or neither. When you write grid-area: some-name, check right then that the parent actually defines that area; when you delete a parent’s named-area definition, the same commit should clear every grid-area referencing it. It’s the relationship between a function and its call sites, except CSS won’t tell you one side went missing.
How to catch it: measure the layout, don’t look at it
When a layout is broken and you can’t find why, this order helps.
Step one: stop looking at screenshots. A screenshot tells you the result is wrong, never which layer is wrong. Fonts, wrapping, width resolution, and track count all look identical in a picture.
Step two: measure the computed style rather than reading the CSS you wrote. One line does it:
getComputedStyle(document.querySelector('.layout-grid')).gridTemplateColumns;
What comes back is the resolved track listing (e.g. "960px 200px 0px 48px"), not your authored minmax(0, 1fr) 200px. Compare the two, and a track count that doesn’t match tells you immediately that the problem lives in grid placement, not in fonts or content width.
This step is the turning point of the whole investigation. Before measuring those 0px 48px tracks, every hypothesis pointed the wrong way. After measuring them, the answer nearly falls out on its own.
Step three: if you’re automating, make it a regression test. This class of bug is easy to reintroduce the next time somebody reworks the layout, and it happens to assert beautifully, because track count is a clean number:
const tracks = await page.evaluate(
() => getComputedStyle(document.querySelector('.layout-grid')).gridTemplateColumns.split(' ').length,
);
// narrow viewport should be a single column
expect(tracks).toBe(1);
Compared with screenshot diffing — which false-positives on fonts, antialiasing, and content changes — counting tracks is far more stable, and the failure message points straight at the cause.
The one-line takeaway
CSS has no undefined. Reference a named area that doesn’t exist and you don’t get “invalid, ignored” — you get a different, fully specified behaviour: implicit tracks. Every tool will tell you the CSS is fine, because as far as the spec goes it is fine. Only the computed style knows what it did.
Next time the CSS looks right and the layout doesn’t, measure before you guess.



