Flexbox and Grid: The Baseline
Years of responsive design practice have left most of us reaching for media queries whenever a layout needs to adapt. But modern CSS — flexbox, grid, clamp(), and relative units among them — offers ways to build responsive layouts with far fewer @media rules. In some cases, a single CSS declaration can replace what would have taken several queries.
The simplest approach uses flexbox with a flexible base width. Setting flex: 400px — shorthand for flex: 1 1 400px — gives each item a 400px basis. Items wrap when they don't fit, stretch to fill leftover space on their line, and shrink back down when another item can squeeze in.
This works in two lines of code but offers no control over the number of items per row, consistent widths across rows, or when wrapping happens.
CSS Grid's repeat(auto-fit, minmax(400px, 1fr)) improves on one front: the last row keeps the same column widths as the rest. It's a single line of code, but items cannot shrink below 400px, which can cause overflow on narrow containers.
Limiting Items Per Row
Adding a max() function to the flexbox base width lets us cap the number of items on each row. Using flex: max(400px, (100% - 20px)/3) means no row ever exceeds three items. Once the container grows wide enough that 100%/3 exceeds 400px, the largest any item can be is one-third of the container — hence three items max. The 20px accounts for two 10px gaps between three items.
A cleaner formula avoids worrying about the gap altogether: max(400px, 100%/(N + 1) + 0.1%). The browser calculates widths for N + 1 items per row, then the tiny 0.1% bump forces one item to wrap, leaving N items per row. The same trick works with the grid minmax() approach, and custom properties make the values easy to adjust.
Allowing Items to Shrink
With grid, using a fixed minmax(400px, 1fr) minimum means items can't go below 400px. Replacing that with a clamp() keeps both the row limit and prevents overflow on small screens:
max(400px, 100%/(N + 1) + 0.1%)
clamp(100%/(N + 1) + 0.1%, 400px, 100%)
When the viewport is wide, 400px gets clamped to 100%/(N + 1) + 0.1%, preserving the maximum items-per-row rule. When the container is narrow, 400px clamps to 100%, so items never exceed the container width.
Controlling Wrap Points
Neither technique so far gives explicit control over when items wrap. Introducing viewport width into the clamp() formula adds a genuine breakpoint:
clamp(100%/(N + 1) + 0.1%, 400px, 100%)
clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)
With this formula, 100vw greater than 400px produces a negative (400px - 100vw), which clamps to the positive 100%/(N + 1) + 0.1% value — N items per row. When 100vw drops below 400px, the difference turns positive; multiplied by a large number, it clamps to 100%, producing a single full-width element per row. The 400px value effectively acts as a media query breakpoint.
Starting from N items per row on wide screens and switching to one below the breakpoint is just a special case. The pattern generalizes to jumping between any two counts, N and M:
clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%/(M + 1) + 0.1%)
Now the layout holds N items per row when the viewport exceeds 400px, and M items otherwise — all from a single declaration driven by custom properties.
Nesting for Multiple Breakpoints
The technique extends further. Nesting a clamp() inside another introduces a second breakpoint, enabling transitions from N to M to one item per row:
clamp(clamp(100%/(N + 1) + 0.1%, (W1 - 100vw)*1000,100%/(M + 1) + 0.1%), (W2 - 100vw)*1000, 100%)
Here, W1 and W2 are the two breakpoint widths. Below W2, the outer clamp resolves to 100% — one item. Between W2 and W1, the outer clamp passes through to the inner one, which yields M items below W1 and N items above it. Two media queries, one CSS rule.
clamp(
clamp(
clamp(100%/(var(--n) + 1) + 0.1%, (var(--w1) - 100vw)*1000,
100%/(var(--m) + 1) + 0.1%),(var(--w2) - 100vw)*1000,
100%/(var(--p) + 1) + 0.1%),(var(--w3) - 100vw)*1000,
100%), 1fr))

N columns to M columns to P columns to 1 columnThere's no obvious limit to the nesting. Each additional clamp() layer adds another breakpoint and another item count — N to M to P to one — still in a single (if long) declaration.
The summary at this point:
- One line of code
- Consistent element widths
- Full control of items per row
- Items grow and shrink
- Control over wrap points
- Easy updates via custom properties
Emulating Container Queries
Every formula above used 100vw, making the layout respond to the viewport. Swap in 100%, and the same rules respond to the containing element's width instead — a crude simulation of container queries, which at this time aren't universally supported. Resizing any container drives its own column count without any relationship to the viewport.
Conditional CSS Without @media
The same clamp()/max() pattern can drive other conditionals. A common request — changing an element's background color based on its width — historically required JavaScript or a media query. A layered background trick does it declaratively:
div {
background:
linear-gradient(green 0 0) 0 / max(0px,100px - 100%) 1px,
red;
}
A gradient layer sized to max(0px,100px - 100%) gives it a non-zero width only when the element is narrower than 100px. When that gradient covers the background, the base red becomes green. The example supports Chrome, Edge, and Firefox. Swapping the gradient dimensions — 1px max(0px,100px - 100%) — makes the condition height-based, and using vw/vh reverts to viewport conditions. Multiple gradient layers extend this to more than two colors.
div {
background:
linear-gradient(purple 0 0) 0 /max(0px,100px - 100%) 1px,
linear-gradient(blue 0 0) 0 /max(0px,300px - 100%) 1px,
linear-gradient(green 0 0) 0 /max(0px,500px - 100%) 1px,
red;
}

Toggling Visibility
Classic show/hide behavior — display: none inside a media query — can be simulated by clamping an element's dimensions to zero:
div {
max-width: clamp(0px, (100vw - 500px) * 1000, 100%);
max-height: clamp(0px, (100vw - 500px) * 1000, 1000px);
overflow: hidden;
}
Based on 100vw, max-width and max-height clamp between 0px (hidden) and 100%/1000px (fully visible). This isn't a true display toggle — the element occupies no space but remains in the document — so it's appropriate for decorative content, not information where accessibility matters. Note that values inside clamp() and max() must be length values; a unitless 0 invalidates the property.
Relocating Elements
Fixed or absolutely positioned elements can also shift at breakpoints using the pattern clamp(X1,(100vw - W)*1000, X2). The difference 100vw - W toggles its sign around width W, and multiplying by a large constant forces the result toward one clamp bound or the other.
div {
--c:(100vw - 400px); /* we define our condition */
top: clamp(10%, var(--c) * -1000, 50%);
left: clamp(0px, var(--c) * 1000, 40%);
}
In this example, the element sits at top: 50%; left: 0 below 400px, and top: 10%; left: 40% above it. Each clamp uses the property's smaller value first, then supplies the condition by the sign of its multiplier — negative to favor the lower value on large screens, positive to favor it on small. The mechanics are fiddly, but the same trick applies to any length-based property: padding, margin, border-width, translate, and others.
Working Demonstrations
These concepts do hold up in real interfaces.
Progress Bar
The background-color switch handles a three-range progress indicator — red from 0% to 30%, orange to 60%, green beyond — with no JavaScript recalculating styles. (Chrome, Edge, and Firefox.)
Editable Content
A contenteditable field can warn visually as content grows: yellow past three lines, red past six. That removes the need to observe height changes and toggle classes. (Chrome, Edge, and Firefox.)
Timeline, Cards, Bubbles, and Buttons
A timeline prototype shifts from two columns with blue/green/green/blue markers to a single column with alternating colors below 600px — no queries. A responsive card demo repositions and resizes an image within the card at narrow viewports instead of simply stacking sections. Speech bubbles and a fixed action button transform at their breakpoints — the button moving from a side-mounted element to a bottom-corner circle — again, with none of the usual @media machinery.
Fixed Alert
A cookie-consent style alert demo puts the same conditional positioning and sizing to work.
Media queries aren't going anywhere; they remain the standards-based answer for many responsive problems. But flexbox, grid, clamp(), and relative units have matured to the point that much of what previously required @media — column counts, wrap points, colors, visibility, positioning — can be expressed in tight, maintainable CSS that stays bound to the content it styles.



