On this page
- Layout used to be a fight. It is not anymore.
- Two tools, one mental model each
- Flexbox in practice: a navbar that just works
- Grid in practice: a gallery with no media queries
- Stop hard-coding sizes: intrinsic sizing and clamp()
- Container queries: components that respond to their own space
- A real responsive card layout, end to end
- Modern selectors: :has, :is, :where
- Logical properties: layout that flips for any language
- Subgrid: when nested items must line up with the parent
- Common mistakes that cost hours
- Takeaways and where to go next
Layout used to be a fight. It is not anymore.
For a decade, CSS layout meant floats, clearfix hacks, and a wall of @media queries tuned to whatever phone was popular that year. In 2026 that is over. Flexbox and Grid are universal, intrinsic sizing keywords let the content decide its own size, and container queries finally let a component respond to *its own* width instead of the browser window. You can build a responsive card grid with zero media queries, and it will be more robust than the old way.
Who this is for
You know the [box model and basic layout](/blog/css-fundamentals-the-box-model-and-layout) but still reach for fixed `px` heights and a pile of breakpoints. You want a mental model for *when* to use Flexbox vs Grid and how to stop hard-coding sizes. By the end you will have a real card layout you can paste into a project today.
The shift in mindset: stop telling the browser exact pixel positions. Describe *relationships*, these grow to fill the row, wrap into as many columns as fit, never get narrower than 16rem, and let the layout engine do the math.
Two tools, one mental model each
Almost every layout question reduces to one decision: am I arranging things along one axis or placing them in two axes at once? That single question picks your tool.
Flexbox lays content out along a line. Grid lays content out into a plane. Reach for the simplest one that fits.
| Aspect | Flexbox | Grid |
|---|---|---|
| Dimensions | One axis (row OR column) | Two axes (rows AND columns) |
| Mental model | Content-out: items size from content, then flex | Layout-in: define tracks first, place items into cells |
| Best for | Navbars, toolbars, button rows, tag lists, centering | Page templates, galleries, dashboards, card grids |
| Alignment | justify-content (main) + align-items (cross) | Same, plus per-track and per-item placement |
| Use when | A single line of things that should grow/shrink/wrap | You need rows and columns to line up together |
They compose
It is not either/or. A common pattern is Grid for the page skeleton and Flexbox for the contents of each cell (a card header with a title that grows and an icon pinned right). Nest freely.
Grid in practice: a gallery with no media queries
Here is the trick that retires most of your breakpoints. repeat(auto-fit, minmax(16rem, 1fr)) tells the browser to make as many equal columns as fit, where each is at least 16rem wide and shares the leftover space equally. On a phone that is one column; on a laptop it might be four, and you wrote one line.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}Unpack minmax(16rem, 1fr): the min (16rem) is the smallest a column may shrink to before the browser drops to fewer columns; the max (1fr) means each column takes an equal share of the remaining space. auto-fit collapses empty tracks so the items that exist stretch to fill the row.
| Keyword | Behaviour with extra space | Use when |
|---|---|---|
| auto-fit | Empty tracks collapse; existing items stretch to fill the row | You want items to grow to use all the width |
| auto-fill | Empty tracks are kept; items stay at their min size | You want a consistent column width even when sparse |
The whole responsive story in one declaration
No `@media (max-width: 768px)`. The column count is a function of available width and your `minmax` floor. Change the floor (`16rem`) to tune how soon it wraps.
Stop hard-coding sizes: intrinsic sizing and clamp()
Fixed sizes (width: 320px, height: 200px) are the root of most layout bugs, they break the moment content is longer or the screen is smaller than you guessed. Intrinsic keywords let the *content* set the size, and clamp() lets a value flex between a floor and a ceiling.
| Keyword | Meaning |
|---|---|
| min-content | The smallest size content fits without overflow (longest word) |
| max-content | The size content wants if never wrapped (whole line) |
| fit-content | Like max-content, but caps at the available space |
Fluid type without breakpoints
clamp(MIN, PREFERRED, MAX) is the single most useful function for responsive type. The preferred value usually mixes a rem base with a viewport unit so it scales smoothly, while the min and max stop it getting unreadable on tiny phones or comically large on 4K monitors.
:root {
/* never below 1rem, scales with viewport, never above 1.25rem */
--step-0: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
--step-2: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
}
body { font-size: var(--step-0); }
h1 { font-size: var(--step-2); line-height: 1.1; }
/* a card that grows with content but never blows past the column */
.card { inline-size: fit-content; max-inline-size: 100%; }Accessibility check
Always keep a `rem` term in the preferred slot so text still scales when a user bumps their browser font size. Pure `vw` (for example `font-size: 4vw`) ignores user zoom and fails [accessibility](/blog/web-accessibility-a11y) expectations.
Container queries: components that respond to their own space
Media queries ask how wide is the *window*, but a card does not care about the window. It cares how much room *it* has. The same card might be full-width in a sidebar-less article and cramped in a three-column dashboard. Container queries finally answer the right question: how wide is my *container*?
Media queries are global. Container queries are local. Truly reusable components measure their container, not the viewport.
You opt an element in as a *containment context* with container-type, then query it with @container. Below, a card lays out vertically by default and switches to a horizontal layout only once its own width passes 28rem, wherever it happens to live.
/* 1. mark the element whose size children will query */
.card-wrap {
container-type: inline-size;
container-name: card;
}
/* 2. default (narrow) layout: stacked */
.card {
display: grid;
gap: 1rem;
}
/* 3. when the CONTAINER is wide enough, go side-by-side */
@container card (min-width: 28rem) {
.card {
grid-template-columns: 8rem 1fr;
align-items: center;
}
}Drop that card into the auto-fit gallery from earlier and something delightful happens: when the grid shows one wide column, each card goes horizontal; when it packs four narrow columns, each card stacks. One component, zero viewport breakpoints, correct in every slot.
Container query units
Inside a container you can size with `cqi` (1% of the container inline size) and friends, fluid sizing scoped to the component, not the page.
A real responsive card layout, end to end
Here is the full thing: an auto-fit Grid of cards, each card a container that flips between stacked and horizontal based on its own width, with fluid type via clamp(). No @media query anywhere.
<ul class="gallery">
<li class="card-wrap">
<article class="card">
<img class="card-img" src="/k8s.png" alt="" />
<div class="card-body">
<h3>Kubernetes Basics</h3>
<p>Pods, services, and your first deployment.</p>
</div>
</article>
</li>
<!-- repeat .card-wrap for each item -->
</ul>.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
list-style: none;
margin: 0;
padding: 0;
}
.card-wrap { container-type: inline-size; }
.card {
display: grid;
gap: 1rem;
padding: 1.25rem;
border-radius: 0.75rem;
border: 1px solid hsl(240 6% 90%);
}
.card-img {
inline-size: 100%;
block-size: auto;
border-radius: 0.5rem;
}
.card h3 { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.4rem); margin: 0; }
/* card goes horizontal once IT (not the window) is wide enough */
@container (min-width: 26rem) {
.card { grid-template-columns: 7rem 1fr; align-items: center; }
.card-img { block-size: 100%; object-fit: cover; }
}- 1
Grid makes the columns
auto-fit + minmax decide how many cards fit per row at any width.
- 2
Each card is a container
container-type: inline-size lets the card measure its own width.
- 3
The card reflows itself
@container flips stacked to horizontal based on the card width, not the page.
- 4
Type stays fluid
clamp() scales the heading smoothly between a readable floor and ceiling.
Modern selectors: :has, :is, :where
Layout is half the story; selecting the right elements without bloated CSS is the other half. Three selectors changed the game. :has() is the long-awaited *parent* selector, style an element based on what it contains. :is() and :where() collapse repetitive selector lists.
/* style the CARD when it contains an image, the parent selector */
.card:has(img) { padding-block-start: 0; }
/* highlight a form field whose input is invalid */
.field:has(input:invalid) { border-color: hsl(0 70% 50%); }
/* :is() groups selectors AND takes the highest specificity inside */
:is(h1, h2, h3) a { color: inherit; }
/* :where() is identical BUT contributes ZERO specificity */
:where(.prose a) { text-decoration: underline; }| Selector | Specificity | Use for |
|---|---|---|
| :is(...) | Highest of its arguments | Grouping when you want normal cascade weight |
| :where(...) | Always zero | Resets and defaults that are trivial to override |
Why :where() is a gift for design systems
Base styles wrapped in :where() add no specificity, so component authors override them with a plain class, no specificity wars, no !important. See [styling strategies: CSS-in-JS, utility, and modules](/blog/styling-strategies-css-in-js-utility-modules) for how this plays with utility and module approaches.
Logical properties: layout that flips for any language
You may have noticed margin-inline-start and inline-size above instead of margin-left and width. Those are logical properties, they describe direction relative to the *text flow*, not the physical screen. In a left-to-right language inline-start is the left; in Arabic or Hebrew (right-to-left) it automatically becomes the right.
| Physical | Logical |
|---|---|
| width / height | inline-size / block-size |
| margin-left / margin-right | margin-inline-start / margin-inline-end |
| padding-top / padding-bottom | padding-block-start / padding-block-end |
| text-align: left | text-align: start |
Why bother if you only ship English today
Internationalization (i18n) is far cheaper when the layout already flips correctly. Set `dir="rtl"` once and a logical-property layout mirrors itself with no extra CSS. It also pairs naturally with block/inline thinking in Flexbox and Grid, and supports inclusive [web accessibility (a11y)](/blog/web-accessibility-a11y).
Subgrid: when nested items must line up with the parent
One last sharp tool. In the card gallery, suppose every card has an image, a title, and a footer button, and you want the titles across *all* cards to line up on the same baseline even when descriptions differ in length. A normal nested grid cannot see the parent tracks. subgrid can: the child adopts the parent grid lines.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
/* shared rows: image / title / text / footer */
grid-template-rows: auto auto 1fr auto;
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 4;
/* inherit the parent's row lines so every card aligns */
grid-template-rows: subgrid;
}Now the four rows are defined once on the gallery, and each card maps its image, title, body, and footer onto those shared lines. Footers align across cards regardless of how much text each one holds, a job that used to require fixed heights or JavaScript.
Check support for your audience
Subgrid and container queries are baseline in all modern evergreen browsers as of 2024-2025. If you must support very old browsers, treat them as progressive enhancement, the layout should still be usable without them.
Common mistakes that cost hours
- Fixed heights on content boxes.
height: 200pxoverflows the instant text is longer. Usemin-heightor let content size the box. - Magic numbers for spacing.
margin-top: 37pxto nudge alignment is a smell, usegap,align-items, ormargin-inline: autoso the engine computes it. - Media-query soup. Five breakpoints to fix one component means you needed
auto-fit/minmaxor a container query, not more breakpoints. - Reaching for Grid to lay out one line. A button row is Flexbox. Grid there is overkill and harder to read.
- Querying the viewport for component layout. A reusable card should use
@container, not@media, otherwise it breaks the moment it moves to a narrower slot. - Physical properties in i18n apps.
margin-leftdoes not flip for RTL;margin-inline-startdoes. - Pure `vw` font sizes. They ignore user zoom; always clamp with a
remterm for accessibility.
Takeaways and where to go next
The whole article in nine lines
- One axis? Flexbox. Two axes? Grid. Compose them by nesting.
- Flexbox is content-out; Grid is layout-in (define tracks, place items).
- `repeat(auto-fit, minmax(16rem, 1fr))` gives a responsive grid with no media queries.
- Let content size things: `min/max/fit-content`; let values flex with `clamp()`.
- Container queries make components respond to their own width, not the viewport.
- `:has()` is the parent selector; `:is()` keeps specificity, `:where()` zeroes it.
- Logical properties (`inline-size`, `margin-inline-start`) flip for RTL for free.
- Subgrid aligns nested items to the parent grid lines.
- Avoid fixed heights, magic numbers, and media-query soup.
Layout is a foundation, not the finish line. Solidify the basics first, then decide how you organize styles at scale, and make sure what you build is usable by everyone.
- Ground the fundamentals: CSS fundamentals: the box model and layout.
- Choose how to organize CSS at scale: styling strategies: CSS-in-JS, utility, and modules.
- Make every layout usable for everyone: web accessibility (a11y).
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.