Fluid Layout First
Percentages, min(), max(), clamp() and the intrinsic behaviour of flex and grid express continuous adaptation; a breakpoint is what you reach for when the change genuinely cannot be continuous.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
How do I build a layout that fits every width, rather than the five widths I happened to test?
Someone opens the product on whatever they have: a phone held in one hand, a laptop window snapped to half the screen, a tablet rotated mid-sentence, a desktop browser at 200% zoom. They expect to read and act without pinching, scrolling sideways, or losing half a label.
Design three layouts — mobile, tablet, desktop — put breakpoints at the widths those devices use, and write fixed pixel sizes inside each. It matches the design handoff exactly, and every value in the CSS is a number a designer chose.
The widths *between* the breakpoints were never designed. At 900px the sidebar is still its fixed 320px and the content column has 540px left, so a table that was comfortable at 1200px now scrolls sideways inside a page that does not.
- The widths *between* the breakpoints were never designed. At 900px the sidebar is still its fixed 320px and the content column has 540px left, so a table that was comfortable at 1200px now scrolls sideways inside a page that does not.
- A snapped browser window, a rotated tablet, a split-screen phone and a picture-in-picture window produce widths no device list contains. The device list describes hardware; the layout responds to a viewport.
- Browser zoom shrinks the viewport measured in CSS pixels, so a desktop user at 200% zoom lands in the phone layout on a 27-inch monitor. That is correct and required behaviour — but a breakpoint named
--tabletmakes it read as a bug and invites someone to "fix" it by suppressing zoom (The Viewport and Device Pixels). - Fixed gutters do not scale down. At a 320 CSS-pixel viewport, 48px of padding on each side leaves 224px for content, and a two-word button label wraps to three lines.
- Every new device class means another breakpoint and another copy of the layout to keep in sync. The stylesheet grows with the device market rather than with the product (Selector Matching Cost).
What is actually happening
In the browser, not in the framework.
- A percentage length resolves against the containing block's size, recomputed during layout. It is a continuous function of available space, not a value you set once.
min(),max()andclamp()are CSS math functions evaluated where a length is allowed.clamp(a, b, c)is exactlymax(a, min(b, c))— a floor, a preferred expression, and a ceiling, all resolved by the same machinery that resolvescalc().- Flex distributes free space:
flex-basisis a starting size andflex-grow/flex-shrinkare ratios applied to whatever is left over or missing. The final width is the output of an algorithm, not a constant (Flexbox: One Axis at a Time). - Grid with
repeat(auto-fit, minmax(min(18rem, 100%), 1fr))makes the *number of columns* a function of width. The track count is computed by the grid algorithm; no query is involved (Grid: Two Dimensions at Once). - Intrinsic sizing keywords —
min-content,max-content,fit-content— let a box be sized by what is inside it rather than by a number you guessed (Intrinsic Sizing and the Automatic Minimum). - A media query is a boolean evaluated against the viewport, so it can only produce a step function. Fluid values produce a continuous one. Most layout change is genuinely continuous; the exception is a change of *arrangement*, which no ratio can express.
What this makes the browser do
And which of it is avoidable.
- Resolving
clamp(),min()and percentages happens inside style resolution and layout, on values the engine was going to compute anyway. Aclamp()is not measurably more expensive than a literal. - Resizing invalidates layout for the affected subtree and the browser lays it out again. A fluid layout does the same work a fixed one does — it just arrives at a different answer, and it arrives at a *new* answer more often.
- Dragging a window fires layout on close to every frame. What makes that expensive is deep nesting of flex inside grid inside flex, where each level must size its children before it can size itself — not the fluid function (Layout Thrashing).
- Content-based track sizing (
auto,min-content,fit-content) forces the engine to measure content before it can size the track. For a handful of cards this is free; for a thousand-row table it is the layout cost (List Virtualization). - Every media query adds a media list re-evaluated on viewport change, which is cheap. The duplicated declarations behind it are not free to download, parse or maintain (The CSSOM).
A range, not a list of devices
The device-first workflow encodes an assumption that is no longer true and arguably never was: that the set of widths a page must handle is small, discrete and knowable in advance. It is none of those. Window snapping, split screen, browser zoom, sidebars, in-app webviews and rotation all produce widths that belong to no device.
The fluid alternative is not "avoid breakpoints". It is to notice that most of what a breakpoint is being asked to do — set a width, a gutter, a font size, a column count — is a continuous relationship that CSS can express directly. What remains after that is a small number of genuine discontinuities, and those are worth a query.
.cards { display: grid; grid-template-columns: 1fr; }
@media (min-width: 768px) { .cards { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .cards { grid-template-columns: 1fr 1fr 1fr; } }
@media (min-width: 1440px) { .cards { grid-template-columns: repeat(4, 1fr); } }
/* four sampled states; every width in between gets the state below it */.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: clamp(0.75rem, 2vw, 2rem);
}
/* one rule; the column count is whatever fits, at every width */The second version states the actual requirement — cards are at least 18rem wide, they fill the row, and they never force the page wider than the viewport — so it is correct at widths nobody tested, including a 340px in-app webview and a desktop window at 250% zoom. The first version encodes four sampled answers to a continuous question and is wrong everywhere between the samples.
The fluid toolkit, in one stylesheet
Four mechanisms cover most of it: a clamped reading measure, a self-sizing grid, a clamped type scale, and a wrapping flex row whose items have a sensible minimum. None of them mentions a device and none of them needs a query.
The pattern to internalise is min(ideal, available) for anything with an upper bound, and clamp(floor, ideal, ceiling) for anything that should scale within limits. Both read left to right as "never smaller than, aim for, never larger than".
1/* Reading column: as wide as the content wants, capped at a readable measure,2 with the gutter subtracted before the cap rather than added after it. */3.page {4 inline-size: min(100% - 2rem, 68ch);5 margin-inline: auto;6}7 8/* Column count is a function of width, computed by the grid algorithm. */9.cards {10 display: grid;11 grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));12 gap: clamp(0.75rem, 2vw, 2rem);13}14 15/* Type that scales between two bounds; the rem term keeps user zoom working. */16.card__title {17 font-size: clamp(1.125rem, 1rem + 1.2vw, 1.75rem);18 text-wrap: balance;19}20 21/* A wrapping row. The min-inline-size is not optional. */22.row { display: flex; flex-wrap: wrap; gap: 1rem; }23.row > * { flex: 1 1 20rem; min-inline-size: 0; }The min(18rem, 100%) inside minmax() is the part people leave out. Without it an 18rem track cannot fit a 320px viewport, the grid refuses to shrink below its minimum, and the fluid layout produces exactly the sideways scroll it existed to prevent. min-inline-size: 0 on the flex children is the same failure in the other layout mode: otherwise a single long unbreakable word sets the row's width.
When a breakpoint is genuinely the right answer
Breakpoints are not the villain; using them for changes that are continuous is. The useful triage question is what *kind* of change this is, because each kind has a different correct tool and a different cost.
Notice that the last option is not about size at all. A hover menu becoming a tap sheet is a change of input, and driving it from width means a laptop user at high zoom loses their hover menu while a tablet in landscape keeps one they cannot use.
Something has to change as space changes. What kind of change is it?
when The thing simply gets bigger or smaller, continuously, with no rearrangement.
cost clamp(), percentages and fr. The cost is arithmetic that is harder to read at a glance than a number, and harder to trace through a chain of custom properties.
when The items are interchangeable and the row should hold as many as will fit.
cost auto-fit with minmax(), or flex-wrap. The cost is that you no longer control the count at any given width, so a design specifying "exactly three across on tablet" cannot be honoured.
when The relationship between regions changes rather than their size: a sidebar moves, tabs become an accordion, a table becomes a list.
cost A media or container query, and genuinely two layouts to maintain, test and keep accessible. The DOM order must make sense in both, because it does not change (Keyboard Operability).
when The same component appears in a wide main column and a narrow sidebar and must fit whichever it got.
cost A container query, plus the containment its parent has to accept (Container Queries).
when What differs is the input device, not the amount of space.
cost A pointer / hover query, never a width query. The cost is that a hybrid device reports its *primary* pointer, which may not be the one currently in the user's hand (Media Queries Beyond Width).
How to build it
Most important first.
- Start from the content, not the device: what is the narrowest width at which this is still usable, and the widest at which it is still worth reading?
clamp()between those, and let the middle be a ratio. - Constrain the reading column with
min(100% - 2rem, 68ch)rather than a stack ofmax-widthoverrides. One declaration replaces three breakpoints and is correct at widths you never tried (Responsive Typography). - Let the layout choose the count.
flex-wrapand gridauto-fitchange how many items sit in a row as a continuous consequence of width — the browser is better at this arithmetic than a query list is. - Reach for a breakpoint only when the *arrangement* changes: a sidebar moving from beside to above, tabs becoming an accordion. Place it where the layout actually breaks, found by dragging the window, not by naming a device (Media Queries Beyond Width).
- When the thing that should drive the change is the component's own box rather than the page's, the query belongs to the container, not the viewport (Container Queries).
- Use logical properties —
inline-size,padding-inline,margin-block— so the same rules survive a right-to-left or vertical writing mode without a second stylesheet (Internationalization).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Zoom is the reason this matters. Browser zoom scales the CSS pixel, so a fluid layout reflows into the narrower viewport automatically; a fixed-pixel layout produces scrolling in two dimensions, which WCAG's reflow criterion treats as a failure. Test by zooming, not only by resizing the window.
- Text-only zoom and a raised default font size scale
remandembut notpx. If the box is sized in pixels and the text inrem, enlarged text overflows or is clipped — sizing the box in relative units is what makes the two move together (Responsive Typography). - Touch targets must not shrink with the viewport. Padding expressed as a percentage gets smallest exactly where fingers are largest; WCAG 2.2 publishes a minimum target size, and a
pointer: coarsefloor is the right place to enforce it (Media Queries Beyond Width). - Reordering visually with
orderor explicit grid placement does not reorder the DOM, so focus order stays at the source sequence while the eye follows the visual one. A keyboard user then tabs around the screen apparently at random (Keyboard Operability). - Content that exists in only one arrangement disappears from the accessibility tree when the layout changes. A desktop-only summary column is genuinely gone for a zoomed-in user, not merely hidden (The Accessibility Tree).
What can go wrong
- A
clamp()whose preferred term never wins:clamp(1rem, 2px, 3rem)is a constant with extra characters. If the middle expression is outside the bounds at every realistic viewport, the fluidity is decorative. - Making things fluid that have a natural size. A 1px border scaled by viewport width, or an icon scaled with the page, produces sub-pixel seams and blurry glyphs for no benefit.
- One non-fluid descendant taking the whole page with it: a fixed-width table, an unbreakable URL, a wide
<pre>. The body scrolls sideways and the fluid parent gets blamed for a child'smin-contentsize. min-width: autoon flex items. A flex item refuses to shrink below its content's minimum size, so a single long word blows out the row.min-inline-size: 0is the fix and is the most common fluid-layout surprise there is.100vwinside a page with a classic scrollbar. The viewport unit ignores the space the scrollbar reserved, so the element is a scrollbar wider than its container and produces exactly the horizontal overflow you were preventing.- The mitigation failing quietly: a layout made fluid but never tested at high zoom. Removing breakpoints does not by itself satisfy reflow — a fluid grid with a
position: fixedheader sized invhstill traps content at 400%.
- A web font arriving after first layout changes text metrics, so any
fit-content,chormax-contentsize is computed twice. The second answer is the one the user reads, and the difference is a visible reflow (Images and Fonts). - An image without an intrinsic ratio finishing after layout resizes the row it is in, moving everything below it. This is the same fluid layout being correct twice with different inputs (Responsive Images).
ResizeObservercallbacks run after layout. Writing a style from one schedules another layout, and if that changes the observed box the browser reports a resize-loop error and drops a frame.
- Layout is not a boundary. A control pushed off screen, clipped by
overflow: hidden, or given zero width is still in the DOM, still focusable, and still backed by an endpoint anyone can call (Authorization-Aware UI). - Fluid positioning can produce accidental overlap: at some widths an absolutely positioned element lands on top of an interactive control, which is the same condition a clickjacking defence worries about, arrived at by accident rather than by an attacker (Clickjacking and Framing).
- User-controlled strings are layout inputs. A 200-character display name in a header sized by content can push the sign-out control out of the viewport — a denial of a control, caused by trusting content length. Constrain with
text-overflowand amin-inline-sizeon the things that matter.
- "Fluid means no media queries." It means media queries carry arrangement changes rather than sizes. A good fluid layout usually has one or two, placed where the content demands them.
- "Percentages are fluid, so use percentages everywhere." Percentage padding and margin resolve against the containing block's *inline* size — including the vertical ones. Everyone discovers this once, usually via a mysteriously tall box.
- "
clamp()replaces breakpoints." It replaces *size* breakpoints. It cannot move a sidebar above the content, and pretending otherwise produces a squeezed two-column layout at phone widths. - "Mobile-first means designing for phones." It means the unqueried base rules are the narrow ones and the queries only add, which keeps the cascade additive and the specificity flat (The Cascade).
- "If it works at 320 and 1920 it works everywhere." The interesting failures live in the middle, which is exactly the region a device-preset workflow never visits.
Measuring it, and what changes in the field
- Drag the window slowly from narrow to wide and watch. The widths at which something first looks wrong are your real breakpoints; this is the entire method, and it takes about ninety seconds (A Method for Frontend Bugs).
- Use the device toolbar in *responsive* mode with a continuously dragged width, not the device presets. The presets are the trap this lesson is about.
- The Performance panel's layout entries during a resize tell you whether a fluid layout is expensive or merely fluid — look for repeated forced layout inside a resize handler (Debugging Rendering and Jank).
- Horizontal overflow is assertable: compare
document.documentElement.scrollWidthwithclientWidthat a set of widths in a headless run and fail the build on the difference (Visual Regression Testing). - In the field, layout-shift attribution names the element that moved and the widths it moved at, which is usually more honest than any local reproduction (Visual Stability).
- On a slow device, an orientation change triggers a full relayout of the document at a new width while the compositor is already busy animating the rotation. Deeply nested fluid layouts are where that becomes a visible stall (The Frame Budget).
- With a large dataset, content-sized tracks force the engine to measure every item to size one column. A fixed or
1frtrack avoids the measurement entirely and is often the whole fix. - On a slow network, a layout that is fluid in CSS but depends on JavaScript to compute a width is fixed until the bundle arrives, then jumps. The fluid version is correct before hydration; the measured version is not (Hydration).
- In a long-lived tab, a window dragged to a monitor with a different pixel ratio re-evaluates media queries and re-selects image candidates, but does not re-run a width you cached in a variable at load (Long-Lived Clients and Version Skew).
- A user whose default font size is large has, in effect, a narrower viewport measured in
chandem. Nothing about their hardware changed; every content-relative measure did.
- Fluid layouts are harder to sign off against a static design file. There is no single width at which the implementation can be compared pixel-for-pixel with a mockup, so the team has to agree on ranges and rules instead of positions (Design Systems).
- The layout is never exactly the designed one at any width — it is *approximately right everywhere* instead of *exactly right at three widths*. That is the correct trade for most products and the wrong one for a print-like marketing page with hand-set typography.
- New bugs become possible: something that breaks only between 812 and 848 CSS pixels cannot happen in a five-breakpoint layout, because those widths do not exist as distinct states there.
clamp()hides its arithmetic. Working out why a heading is at 22px by reading three nested clamps and a custom property chain is genuinely worse than reading a media query, and it is the main reason teams drift back to breakpoints (Custom Properties).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALPercentages,
calc(),min(),max(),clamp(), flex and grid intrinsic sizing are interoperable across Blink, Gecko and WebKit and behave the same way; disagreements in this area are almost always about scrollbars or sub-pixel rounding, not about the algorithms themselves. - DEVICE-SPECIFICWhether
100vwoverflows depends on how the platform draws scrollbars: desktop engines that reserve classic scrollbar space make100vwwider than the content box, while mobile and overlay-scrollbar platforms reserve nothing, so the bug appears on one class of machine and is invisible on another.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — "express the rule, not the sampled outputs" is the same argument as replacing a lookup table with the function that generated it; a layout is a small program and breakpoints are its memoised special cases.
- — Testing & Reliability Engineering — asserting no horizontal overflow across a swept range of widths is a property test, and it catches the class of bug that fixed-width visual snapshots structurally cannot.