Eqii
Design Intermediate 12 min read

Modern CSS Layout: Flexbox, Grid, and Container Queries

Flexbox, CSS Grid, subgrid, container queries, logical properties, and the new viewport units. A practical tour of the layout system modern CSS actually offers, with a decision tree for choosing the right tool.

The layout problem Flexbox and Grid actually solve

For fifteen years, CSS layout was a collection of hacks. We used floats, cleared them with overflow tricks, built multi-column layouts with `display: table` because it was the only thing that worked, and prayed for the day Internet Explorer would die. Flexbox and CSS Grid changed that. They are not competing tools — they solve different problems, and a senior CSS author uses both in the same project every day.

The distinction is dimensional. Flexbox is one-dimensional: it lays items out along a single axis, either a row or a column. CSS Grid is two-dimensional: it lays items out along both rows and columns simultaneously. If you are building a navigation bar with five items in a row, you want Flexbox. If you are building a page with a header, sidebar, main content area, and footer, you want Grid. Container queries, the third major modern addition, let a component adapt its layout based on the size of its container rather than the size of the viewport — which is the right model for design systems and reusable components.

This guide walks through the three layout systems, the modern units and properties that make them work, and the decision process for choosing between them.

Flexbox: the one-dimensional workhorse

Flexbox is the layout system you reach for when you have a row or a column of items and you want to control how they are distributed, aligned, and sized within that line. The mental model is two axes: the main axis (the direction the items flow) and the cross axis (perpendicular to it). When you set `display: flex` on a container, the main axis defaults to `row` (horizontal); `flex-direction: column` flips it to vertical.

The four properties that do most of the work are `justify-content`, `align-items`, `gap`, and `flex` (the shorthand applied to children). `justify-content` controls distribution along the main axis: `flex-start`, `flex-end`, `center`, `space-between`, `space-around`, `space-evenly`. `align-items` controls alignment along the cross axis: the same options plus `stretch`, which is the default and makes children fill the cross axis. The combination of those two properties covers an enormous range of layouts.

The `gap` property, originally a Grid feature, now works in Flexbox and removes the need for negative margins or `:last-child` selectors. `gap: 12px` puts twelve pixels of space between every flex item, and there is no trailing gap to worry about. This alone justifies using Flexbox for almost any horizontal or vertical stack.

The `flex` shorthand on children takes three values: `flex-grow`, `flex-shrink`, and `flex-basis`, in that order. `flex: 1` is shorthand for `flex: 1 1 0%`, which means the item can grow and shrink, and its base size is zero — so it shares space equally with other `flex: 1` siblings. `flex: 0 0 auto` means the item takes its content size and does not grow or shrink, which is what you want for icons and labels in a toolbar. `flex: 1 0 200px` means grow to fill available space, never shrink below 200 pixels, and use 200 pixels as the basis. Most layout bugs in Flexbox come from developers using `flex: 1` without understanding what the third value controls.

A common pitfall: `min-width: auto` on flex items means they will not shrink below their content's intrinsic minimum size. If you have a flex item containing a long unbroken word, the item will overflow rather than shrink. The fix is `min-width: 0` (or `min-height: 0` for column layouts), which lets the item shrink properly. This single rule solves more Flexbox bug reports than any other.

CSS Grid: two-dimensional layout done right

CSS Grid shines when you need to control both rows and columns at once. A page layout — header across the top, sidebar on the left, main content filling the rest, footer at the bottom — is the canonical example, and Grid handles it in five lines of CSS.

The key property is `grid-template-columns` (and its sibling `grid-template-rows`). You can specify columns as fixed sizes (`200px 1fr 200px`), as a repeated pattern (`repeat(3, 1fr)`), or with the `auto-fit` and `auto-fill` keywords that create responsive grids without media queries. `grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))` is one of the most useful lines in modern CSS: it creates as many columns as will fit, each at least 240 pixels wide, and stretches them to fill the container. Resize the container and the columns reflow automatically.

The `fr` unit is unique to Grid. It represents a fraction of the available space in the grid container, after fixed-size tracks are accounted for. `1fr 2fr 1fr` divides the remaining space into quarters and gives the middle column twice as much as the sides. `fr` is not a length; it is a distribution ratio, and it interacts with `minmax()` in powerful ways.

Placement of items is explicit and readable. `grid-column: 1 / -1` makes an item span from the first column to the last, regardless of how many columns there are. `grid-area: header` lets you name regions of the grid and assign items to them by name, which makes the layout self-documenting. The `grid-template-areas` property lets you draw the layout as ASCII art:

``` .layout { grid-template-areas: "header header" "sidebar main" "footer footer"; } ```

That is more readable than any layout abstraction built out of floats or flex.

One feature that catches new Grid authors: implicit rows. If you place more items than the grid has cells defined, Grid auto-creates new rows to hold them, with sizes controlled by `grid-auto-rows`. A common pattern is `grid-auto-rows: minmax(120px, auto)` so each auto-created row is at least 120 pixels tall but can grow with its content.

Subgrid: the missing piece

For years, the gap in CSS Grid was subgrid. You could nest a grid inside a grid, but the inner grid had its own track sizes — it could not inherit the outer grid's columns or rows. That made aligning content across nested elements painful: a card with a header, body, and footer where you wanted all the headers aligned across cards required either fixed heights or JavaScript.

Subgrid fixes this. Set `grid-template-columns: subgrid` on a child of a grid container, and the child inherits the parent's column tracks. A row of cards, each with a header, two body lines, and a footer, can now align their internal sections across cards using the parent grid's tracks. Browser support for subgrid landed in all major engines in 2023 and is now safe to use.

The pattern looks like this:

``` .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; } .card { display: grid; grid-template-rows: subgrid; grid-row: span 3; } ```

Each card spans three rows of the parent grid (its header, body, and footer rows), and uses `subgrid` so its own rows line up with the parent's. The result is a card grid where every card's header is aligned with every other card's header, no matter how much content varies. Before subgrid, this required fixed heights or JavaScript measurement; now it is one property.

Container queries: components that respond to their parent

Media queries are scoped to the viewport. That was fine when components lived in fixed page layouts, but it breaks down in a component-based world. A sidebar card may sit in a 320-pixel column on desktop and in a full-width container on mobile. The viewport is the same; the available space is different. The component needs to respond to its container, not the viewport.

Container queries fix this. You declare a container with `container-type: inline-size` (or the shorthand `container-type: size` for both dimensions), then query it with `@container`:

``` .sidebar { container-type: inline-size; } .card { display: block; } @container (min-width: 400px) { .card { display: grid; grid-template-columns: 1fr 2fr; } } ```

The card now switches from stacked to side-by-side when its container (the sidebar) is at least 400 pixels wide, regardless of viewport. This is the right primitive for design systems: components are reusable across layouts that may have very different available widths, and the component itself decides how to behave.

Container query units — `cqw`, `cqh`, `cqi`, `cqb`, `cqmin`, `cqmax` — let you size things relative to the container, the way `vw` and `vh` size relative to the viewport. A headline that scales with its container (`font-size: clamp(1.5rem, 5cqi, 3rem)`) is a powerful primitive for component design. Browser support for container queries is universal in 2025.

One thing container queries do not solve: responding to the amount of content, rather than the size of the container. There is no `@container (min-content-block-size: ...)` query. If you need that, you are back to JavaScript measurement via ResizeObserver. Container queries handle the common case (responsive component layout) cleanly; they do not handle every case.

Logical properties and modern units

Two related modernizations round out the toolkit: logical properties and the new viewport units.

Logical properties replace physical properties like `margin-left` and `padding-right` with `margin-inline-start` and `padding-inline-end`. The difference is that logical properties respect the writing direction. In a left-to-right language like English, `margin-inline-start` is the left side. In a right-to-left language like Arabic, it is the right side. If you build a component with logical properties and then ship an Arabic translation, the layout flips correctly with no extra CSS. There is a logical equivalent for nearly every physical property: `width` becomes `inline-size`, `height` becomes `block-size`, `top` becomes `inset-block-start`, and so on.

The viewport units — `vw`, `vh` — have a long-standing problem on mobile browsers: the browser chrome (address bar, toolbars) appears and disappears as the user scrolls, which changes the viewport height. A layout sized to `100vh` will be taller than the visible area when the chrome is visible, which means the bottom of the page is hidden behind the toolbar. The new units solve this:

  • `dvh` (dynamic viewport height) updates as the browser chrome appears and disappears. Your `100dvh` layout always fits the visible area.
  • `svh` (small viewport height) is the smallest possible viewport, with all browser chrome visible. Use it when you need to guarantee something is always visible.
  • `lvh` (large viewport height) is the largest possible viewport, with all chrome hidden. Use it when you want to fill the screen and accept that the bottom may be hidden behind transient UI.

For full-screen layouts — modals, drawers, splash screens — prefer `dvh` over `vh`. The old behavior is rarely what you want.

Choosing between Flexbox, Grid, and container queries

The decision tree is short. Ask one question: am I laying things out in one dimension or two?

If the answer is one dimension — a horizontal nav bar, a vertical stack of cards, a row of buttons in a toolbar — use Flexbox. The `justify-content` and `align-items` properties give you everything you need, and `gap` handles spacing cleanly.

If the answer is two dimensions — a page layout, a photo gallery, a complex card composition with aligned sections — use Grid. The `grid-template-columns` and `grid-template-areas` properties are more expressive than anything Flexbox can do for two-dimensional layouts, and subgrid closes the gap for nested alignment.

Use container queries whenever a component needs to respond to its own width rather than the viewport. If you find yourself writing `@media (min-width: 768px)` inside a component, ask whether the breakpoint is really about the viewport or about the space the component happens to have. Almost always, it is the latter, and a container query is the correct primitive.

A few rules of thumb that hold up in practice:

  • Default to Flexbox for the inside of a component and Grid for the outside.
  • Reach for container queries whenever the same component renders in multiple layouts.
  • Use logical properties in any code you expect to internationalize.
  • Stop using `vh` for full-screen layouts; use `dvh`.
  • Stop writing media queries for component-level breakpoints; use `@container`.

Modern CSS gives you tools that are more capable, more readable, and more maintainable than the float hacks of a decade ago. The challenge is no longer working around the platform — it is learning the platform well enough to use it directly. Spend a weekend building something with Grid, subgrid, and container queries, and you will not go back.