Some layout bugs never appear while you build. The page looks fine in a desktop window, the build is green, and the console is quiet. Then a visitor on a narrow phone finds a code sample sliced off with no way to scroll to it, or a page whose theme vanishes a second after it appeared. Two families of causes account for many of these: a grid column that refuses to get narrower than its content, and a component that renders differently on the server than in the browser. This guide explains both, shows a third small trap with environment variables, and ends with a check that can fail.
The grid column that will not shrink
Most developers read 1fr as "one share of the free space, and never mind the content". The specification says something slightly different. A flexible track written on its own, outside the minmax() function, implies an automatic minimum, which means it behaves as minmax(auto, 1fr) (W3C, CSS Grid Level 1). The automatic minimum is where the surprise comes from. For a grid item that is not a scroll container and sits in a track with an automatic minimum, the minimum size becomes the item's content-based minimum, not zero (W3C, CSS Grid Level 1).
In plain words: if the item contains something that cannot wrap, such as a long URL, a token, a table, a block of preformatted text or an image with a fixed width, the column will be as wide as that thing, even when the screen is narrower. MDN describes the automatic minimum the same way, as the largest minimum size of the items in the track (MDN, minmax()).
Written out as a stylesheet, the usual form of the bug is two lines:
.page { overflow-x: clip; }
.layout { display: grid; grid-template-columns: 14rem 1fr; }
If the second column holds a wide pre, the track grows with it. Without the clip, the document gets a horizontal scrollbar and you notice. With overflow-x: clip on an ancestor, nothing scrolls and nothing complains: the overflowing part is simply cut off. A check that compares the width of the document with the width of the window then reports a clean page, because the document really is not wider. That is why this bug survives automated checks.
The fix has two halves
The first half is to give the track a minimum of zero, with minmax(0, 1fr). MDN uses exactly that form in its own example of equal columns (MDN, minmax()). Giving the grid item min-width: 0 has a similar effect. The second half is easy to forget, and it is general practice rather than something the cited pages say: the narrower column still has to cope with its content. Text needs a rule that lets long strings break, such as overflow-wrap: anywhere. A pre or a table needs its own scrolling, for example overflow-x: auto. If you only narrow the track, an unbreakable string simply runs past the edge of its column, and a clipping ancestor still cuts it.
The spec also tells you when the bug does not occur: an item that is a scroll container has an automatic minimum of zero, so a pre that already scrolls inside its own box is not the problem. As far as we remember, Tailwind's grid-cols-* utilities are written with minmax(0, 1fr), which would make the trap mostly a hand-written CSS problem; we did not check that against its documentation, so look at the CSS it generates.
To test it, do not compare the document width. Walk the elements that could be wide, and compare how far each one reaches to the right with the width of the viewport, at a narrow window size.
The component that renders twice
The second family needs a short explanation of hydration. Next.js sends HTML from the server, and React then attaches to it in the browser. React expects the first browser render to match what the server produced. Its documentation says plainly that mismatches should be treated as bugs and fixed, and that there is no guarantee attribute differences will be patched up (React, hydrateRoot).
The Next.js documentation lists the common causes of a mismatch. Two are relevant here: browser-only APIs such as window in the rendering logic, and time-dependent code such as the Date() constructor in the rendering logic (Next.js, hydration error).
Dates are the classic case
A client component that takes a stored timestamp and formats it with toLocaleDateString is a mismatch waiting for the right hour. The server and the browser may use different time zones, so around the turn of a day they can disagree about which day it is. We have not found the time zone of a typical server stated in the cited pages. It is common for a hosted function to run in UTC, but treat that as general practice and check your own host. When the two renderings differ, you get the warning in development; in production the visible symptom depends on how the mismatch is handled, and a page can appear to reset itself.
One consequence is easy to miss. Anything you changed on the page outside React, such as a theme class added to the html element by a small inline script or a class that locks scrolling, can be lost when the tree is rebuilt. That is an inference from how hydration works, not a statement from the cited pages, so reproduce it before you rely on it. A page that resets only on one route is a good hint to look at that route's client components for dates.
The Next.js page offers escape hatches: render the differing part only after the first browser render, turn off server rendering for that component, or add suppressHydrationWarning to an element, which the page describes as a one-level-deep escape hatch that should not be overused (Next.js, hydration error). They work, but they hide the question. A cleaner fix: build the text of the date where the data is loaded, and hand the component that ready-made text, so the browser never formats a timestamp at all.
Environment variables in the browser
Values from environment files are available only on the server unless their name begins with NEXT_PUBLIC_. The documentation states that other variables are not accessible to the browser, and that the prefixed ones are inlined into the JavaScript bundle at build time (Next.js, environment variables). So a client component that reads an unprefixed variable gets undefined.
This becomes quiet when a default stands behind the read, for example a schema default or a fallback literal. Everything seems fine, because the fallback is used. On a staging site it may be the production address, or something your security policy blocks, and on production the fallback may happen to equal the right value, so nobody sees a problem. Our advice, from experience rather than from the documentation: let a missing value throw or log in the browser, and open the staging site, not only production.
A check that can fail
Each fix should earn your trust by failing first, then passing:
- Load the page at a narrow window width and see how far each wide element reaches past the viewport edge. Put a long unbroken string in the content on purpose.
- Remove your fix once and confirm the check fails. If it stays green with the grid bug put back, it does not measure anything.
- Search the client components for date formatting calls and for reads of unprefixed environment variables.
- Make the server and the browser disagree about the time zone: run the server with a different
TZvalue than the browser uses, or compare a deployed build with a browser set to a zone several hours away, then look for hydration warnings. Changing the zone of one machine that runs both server and browser moves both together, and the check can then never go red.
The habit of proving that a check can go red also applies to code review done by a model; our report on a model reviewing Cloudflare Workers code shows what happens when the defects stay invisible in development.
When this does not apply
Pages made only of server components have no hydration step with anything to mismatch, and layouts built with flexbox follow minimum-size rules of their own that we did not look at. If every grid item can wrap and your tracks already use minmax(0, 1fr), skip the first half. We ran no experiments of our own for this piece, which restates the specification and the framework documentation, and the places marked as general practice or inference are our own reading, so try them in your browsers and on your host. Both browsers and frameworks move on, so recheck the details on the cited pages.
Checking your own code
For a review of pasted code, our frontend-layout-review skill reads CSS, Tailwind classes, JSX or a client component. It reports defects of the kind covered above: an unguarded flexible track beside content that cannot wrap, dates formatted in the browser, unprefixed variables read in client code, and a few neighbouring layout traps. Every finding names its location and a fix, and code with nothing wrong gets the single reply "No findings". The skill is sold in the store at https://aiskills402.com.
Read this post as Markdown: /blog/css-grid-hydration-layout-bugs.md · Atom feed.
