Back to Blog
Frontend16 min readJun 2026

Modern CSS Layout: Flexbox, Grid, and Container Queries

Lay out UIs the 2026 way. When to reach for Flexbox versus Grid, intrinsic sizing with clamp and minmax, and component-level responsiveness with container queries, no media-query soup.

CSSFlexboxGridResponsive
SB

Sri Balaji

Founder

On this page

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.

A row of books on a shelf, they grow, shrink, and shuffle along the shelf to fitFlexbox: one-dimensional, content flows and flexes along a single axis
A chessboard, you place pieces onto named squares by row and columnGrid: two-dimensional, you define tracks and drop items into cells
A bookshelf that adds a whole new shelf when books overflowflex-wrap / grid auto-fit: items wrap onto new lines automatically
Pick the tool by how you think about the space, not by the syntax.
Flexbox lays content out along a line. Grid lays content out into a plane. Reach for the simplest one that fits.
AspectFlexboxGrid
DimensionsOne axis (row OR column)Two axes (rows AND columns)
Mental modelContent-out: items size from content, then flexLayout-in: define tracks first, place items into cells
Best forNavbars, toolbars, button rows, tag lists, centeringPage templates, galleries, dashboards, card grids
Alignmentjustify-content (main) + align-items (cross)Same, plus per-track and per-item placement
Use whenA single line of things that should grow/shrink/wrapYou need rows and columns to line up together
Flexbox vs Grid at a glance.

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.

Flexbox in practice: a navbar that just works

A navigation bar is the textbook Flexbox case: a single row where the logo sits left, links sit in the middle, and an action button hugs the right. gap handles spacing, margin-inline-start: auto pushes the trailing item away, no floats, no absolute positioning.

html
<nav class="nav">
  <a class="brand" href="/">Cloud<span>Learn</span></a>
  <ul class="nav-links">
    <li><a href="/blog">Blog</a></li>
    <li><a href="/labs">Labs</a></li>
    <li><a href="/paths">Paths</a></li>
  </ul>
  <a class="cta" href="/signup">Sign up</a>
</nav>
css
.nav {
  display: flex;
  align-items: center;   /* vertical centering on the cross axis */
  gap: 1.5rem;           /* spacing between every child, no margins */
}

.nav-links {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* push the CTA to the far end of the row */
.cta { margin-inline-start: auto; }

Three things do the heavy lifting: display: flex makes children sit on one line, align-items: center centers them vertically without a single px of guesswork, and margin-inline-start: auto eats all the free space before the button. Resize the window and it stays correct, Flexbox recomputes the free space on every frame.

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.

KeywordMeaning
min-contentThe smallest size content fits without overflow (longest word)
max-contentThe size content wants if never wrapped (whole line)
fit-contentLike max-content, but caps at the available space
Intrinsic sizing keywords, let content decide.

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.

css
: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.
Why container queries changed layout

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.

css
/* 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.

html
<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>
css
.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. 1

    Grid makes the columns

    auto-fit + minmax decide how many cards fit per row at any width.

  2. 2

    Each card is a container

    container-type: inline-size lets the card measure its own width.

  3. 3

    The card reflows itself

    @container flips stacked to horizontal based on the card width, not the page.

  4. 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.

css
/* 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; }
SelectorSpecificityUse for
:is(...)Highest of its argumentsGrouping when you want normal cascade weight
:where(...)Always zeroResets and defaults that are trivial to override
:is() vs :where(), the difference is specificity.

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.

PhysicalLogical
width / heightinline-size / block-size
margin-left / margin-rightmargin-inline-start / margin-inline-end
padding-top / padding-bottompadding-block-start / padding-block-end
text-align: lefttext-align: start
Physical vs logical, same effect in LTR, but logical adapts.

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.

css
.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

  1. Fixed heights on content boxes. height: 200px overflows the instant text is longer. Use min-height or let content size the box.
  2. Magic numbers for spacing. margin-top: 37px to nudge alignment is a smell, use gap, align-items, or margin-inline: auto so the engine computes it.
  3. Media-query soup. Five breakpoints to fix one component means you needed auto-fit/minmax or a container query, not more breakpoints.
  4. Reaching for Grid to lay out one line. A button row is Flexbox. Grid there is overkill and harder to read.
  5. 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.
  6. Physical properties in i18n apps. margin-left does not flip for RTL; margin-inline-start does.
  7. Pure `vw` font sizes. They ignore user zoom; always clamp with a rem term 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.

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.