Modern CSS Layout: Flexbox, Grid, and Container Queries
Still centering with float and margin? Modern CSS has moved on. This article covers the division of labor between Flexbox (1D) and Grid (2D), modern centering/equal-height/holy-grail patterns, Container Queries (component-level responsiveness), and new CSS capabilities (@layer/:has/logical properties)—so you write stronger layouts with less code.
Key Points
- Topic Focus: React、TypeScript
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
If you're still writing display: inline-block plus vertical-align: middle, or using float: left together with an overflow: hidden clearfix — this article is for you.
Modern CSS is long past the hack-stitched form it had a decade ago. Flexbox, Grid, Container Queries, @layer, :has, logical properties… these aren't "trendy toys," they're productivity tools you can use directly in today's browser baseline. One obvious feeling: many layouts that previously required a JS library (or a pile of BEM + SCSS nesting) can now be written in a few lines of pure CSS, and more stably and maintainably.
This article explains the pieces you use most day to day: Flexbox, Grid, Container Queries, and a few game-changing new features.
1. Flexbox: The Swiss Army Knife for One-Dimensional Layout
Flexbox solves one-dimensional layout problems—a row or a column. Its mental model is simple: the container is the flex container, its children are flex items, you pick a "main axis" (row or column), and the rest of the children lay out along it.
.toolbar {
display: flex;
flex-direction: row; /* main axis */
align-items: center; /* cross-axis alignment */
justify-content: space-between;
gap: 12px;
}
A few key concepts to learn once:
justify-contentcontrols distribution along the main axis (flex-start / center / space-between / space-evenly).align-itemscontrols alignment along the cross axis (defaultstretch, often changed tocenterorflex-start).gapis the spacing property Flexbox finally supports—stop pushing with margin.gapdoesn't produce extra spacing at the start and end, and changing it is done in one place.flex: 1is shorthand forflex-grow: 1; flex-shrink: 1; flex-basis: 0%, meaning "I'll take all the remaining space."
Typical scenarios: top navigation, action button toolbars, card headers (icon + title + action), any linear single-row or single-column arrangement.
.nav {
display: flex;
align-items: center;
gap: 24px;
}
.nav .logo { flex: 0 0 auto; }
.nav .menu { flex: 1; } /* menu eats all the middle space */
.nav .actions { flex: 0 0 auto; }
A common pitfall: flex-grow only divides remaining space. If the items already fill the container, flex: 1 appears to do nothing. To truly "split equally," remember flex-basis: 0 (i.e., just write flex: 1).
2. Grid: The Right Answer for Two-Dimensional Layout
Whenever a layout involves both rows and columns at the same time, it's Grid's turn. Flexbox in two-dimensional scenarios forces you to nest many layers of containers, while Grid lays it out clearly in one.
.page {
display: grid;
grid-template-columns: 240px 1fr 200px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Area naming (grid-template-areas) is Grid's most underrated capability. Glance at the CSS and the whole page skeleton appears—far more intuitive than a blob of grid-column: 1 / 4, and to change it you only touch the ASCII diagram.
Another killer feature is auto-fit + minmax, writing a "self-adapting card grid" in one line:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
This line means: each column is at least 280px; as the container widens, more columns are added automatically; any leftover space is distributed evenly across all columns. No media queries, no JS width measurement—the card grid automatically goes from one column to two, three, four. The difference between auto-fit and auto-fill in one sentence: auto-fit collapses empty tracks (letting existing cards fill out), while auto-fill keeps empty tracks. For 99% of card scenarios you want auto-fit.
3. Flexbox vs Grid: Stop Picking Sides
The community always asks "which one should I use?" The answer depends on dimensions:
- One-dimensional (content in one row or column squeezing each other, distributing space) → Flexbox.
- Two-dimensional (planning both rows and columns at once, e.g., the whole page skeleton, a two-column form) → Grid.
In practice the two are often combined: the outer Grid divides the page into regions, the inner Flexbox handles linear arrangement within components. There's no contradiction.
.card {
display: flex; /* inside component: one-dimensional */
flex-direction: column;
gap: 12px;
}
.card-deck {
display: grid; /* outer: two-dimensional card grid */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
The rule of thumb is simple: draw a picture in your head—if counting one row or one column is enough, use flex; if you need to consider rows and columns together, use grid.
4. Centering: A One-Liner
"How to vertically center" used to be the classic torture question in CSS interviews. Now? One line:
.center {
display: grid;
place-items: center;
}
place-items is shorthand for align-items and justify-items; under Grid it controls both directions at once. Flexbox can also do it in two lines:
.center-flex {
display: flex;
align-items: center;
justify-content: center;
}
As for the old absolute-positioning + transform: translate(-50%, -50%) pattern—still used, but only when you genuinely need to "leave the document flow and position relative to any ancestor." For everyday centering, one line of Grid is enough.
5. Container Queries: Component-Level Responsiveness
This is one of the most important CSS updates in recent years.
@media queries the viewport width, but the component you wrote has no idea whether it's been placed in a 1200px main area or a 320px sidebar. The same "user card" should be horizontal in the desktop main area and vertical when stuffed into a mobile sidebar—but the viewport hasn't changed, and @media is powerless. So developers end up stuffing props into components or resorting to ResizeObserver.
Container Queries let components respond based on their parent container's width:
.card-host {
container-type: inline-size; /* declare this is a container */
}
@container (min-width: 480px) {
.user-card {
display: flex;
flex-direction: row;
gap: 16px;
}
}
@container (max-width: 479px) {
.user-card {
display: flex;
flex-direction: column;
gap: 8px;
}
}
This is revolutionary for component libraries—cards, tables, panels, those "reused everywhere" components, can for the first time truly "look right wherever I am," without the consumer passing in compact / variant. Paired with container-query units like cqw and cqh, font sizes and spacing can scale fluidly with the container size.
The mental shift in one sentence: @media looks at the viewport, @container looks at the container. Use container for component-internal layout, use media for page-level overall responsiveness.
6. New CSS Capabilities: Three Features Worth Using Right Now
1. @layer: Cascade Control
In large projects CSS ordering is a mess: third-party libraries, design system resets, business styles, override patches all pile together—last writer wins. @layer gives you explicit priority:
@layer reset, framework, components, utilities;
@layer reset {
* { box-sizing: border-box; margin: 0; }
}
@layer framework {
/* third-party / design system styles */
}
@layer components {
.btn { /* ... */ }
}
@layer utilities {
.text-center { text-align: center; }
}
Declaration order determines priority; layers declared later have higher overall priority. This means a .hidden in the utilities layer can reliably override a .btn in the components layer, without !important slugfests. The "specificity war" finally has a peace settlement.
2. :has(): The Parent Selector
The "select a parent based on its child" feature CSS has waited twenty years for:
/* when a card contains an image, enlarge the title */
.card:has(img) .title { font-size: 1.5rem; }
/* when a form item errors, highlight the whole group */
.form-group:has(.error) { border-color: red; }
/* the panel corresponding to the selected tab */
.tab:has(*[aria-selected="true"]) { /* ... */ }
Previously these required JS to add classes. :has() returns a large class of "state-linked styles" to CSS's core job; component libraries and form scenarios benefit especially. Note that :has() is relatively expensive internally, but modern browsers have done extensive optimization, so routine usage needs no performance worry.
3. Logical Properties: Built-in Internationalization
margin-left is a physical direction; under RTL languages like Arabic / Hebrew it flips entirely and misaligns. Logical properties follow the writing direction:
.badge {
inset-inline-start: 12px; /* replaces left, auto-becomes right under RTL */
padding-inline: 16px; /* replaces padding-left + right */
margin-block-end: 8px; /* replaces margin-bottom */
border-inline-start: 1px solid #ddd;
}
inline is the text-flow direction (horizontal in horizontal writing), block is the block-stacking direction (vertical). start / end replace left / right. Once a project needs internationalization, this set of properties saves countless special patches under dir="rtl". Start using logical properties on day one of a new project—almost zero cost, huge payoff.
Summary
- Flexbox handles one dimension, Grid handles two—don't pick sides, combine often. Rule of thumb: counting one row/column → flex; planning rows and columns together → grid.
- Centering is a
display: grid; place-items: center;one-liner; reserve absolute positioning + translate for scenarios that truly need to leave the document flow. auto-fit + minmaxwrites a self-adapting card grid in one line, replacing a pile of media queries.- Container Queries let components respond based on the container rather than the viewport, the true remedy for component-library reuse; use
@mediaat page level,@containerat component level. @layer,:has, logical properties solve the hard problems that previously required JS or!importantslugfests; new projects should adopt them directly.
A closing line: the biggest change in modern CSS isn't a few new properties, but returning to CSS itself the things that "used to require JS and hacks to get right"—write less code, get stronger layouts.
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.
Article
What Value Do Server Components Offer Content Sites?
Technical trends and engineering practices around Next.js and React, from an independent developer's perspective.