Topic 03

Fluid Layouts Without Breakpoints

CSSHands-on

Media queries change a layout at fixed points. Between those points, nothing adapts, and every new breakpoint is one more rule to keep in step with the others. The best responsive layouts need very few, because their parts stretch and shrink on their own. A handful of CSS functions make sizes fluid, a single grid line decides its own number of columns, and container queries let a component respond to the space it was actually given.

Think of a folding table with extra leaves. It opens out to fit the room it is in, and nobody has to measure the room first or keep a list of room sizes. Fluid CSS works the same way: you describe the limits, and the browser fits the layout to the space.

min, max and clamp

Three CSS functions choose a value for you. min() takes a list of values and uses the smallest, max() uses the largest, and clamp() takes three: a minimum, a preferred value and a maximum, and keeps the preferred value between the other two.

css/style.css
main {
  width: min(100%, 60rem);
  margin-left: auto;
  margin-right: auto;
}

This gives the main element whichever is smaller: the full width available, or 60 rem. On a phone, 100 percent is smaller, so the content fills the screen. On a wide monitor, 60 rem is smaller, so the content stops growing at a comfortable width, and the two automatic margins share the leftover space equally, centring it. One line replaces what would otherwise be a default and a media query.

Fluid Type, Safely

A large heading that looks right on a laptop can take up half a phone screen. One idea is to size it in vw, where one vw is one percent of the window's width, so the heading grows and shrinks with the window. On its own, that breaks something important: when a reader zooms the page with the browser's zoom controls, a size in vw stays the same, because zooming does not change the proportions of the window. Text that cannot be enlarged fails people with low vision, which Chapter 12 measures properly.

The safe version mixes the two kinds of unit and puts limits on the result.

css/style.css
h1 {
  font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
}

Read it from the middle out. The preferred size is 1.2 rem plus 2 percent of the window's width, so it grows smoothly as the window widens. The first value is the floor: the heading is never smaller than 1.75 rem. The last value is the ceiling: it is never larger than 3 rem. On a phone the heading sits at its floor, on a laptop it is somewhere in between, and on a wide monitor it stops at its ceiling. Because the rem part follows the reader's settings and the limits are in rem, zooming in still makes the heading bigger.

A Grid That Counts Its Own Columns

The site's project grid currently has two rules: one column by default, and a media query that switches to three at 40 rem. Grid can make that decision by itself.

css/style.css
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-l);
}

Replace the .project-grid rule and delete the width query from the last topic that went with it; the dark-theme query stays. Instead of a number, the first argument to repeat() is the keyword auto-fit: make as many columns as will fit. Each column is at least 16 rem wide and otherwise takes an equal share. On a phone, only one 16 rem column fits, so the cards stack. On a typical tablet, two fit, and the third card starts a second row. On a laptop there is room for three side by side.

There is no breakpoint anywhere, and the layout is right at every width in between, not only at the ones someone thought to test. The word fit matters too: with only three cards and room for five columns, auto-fit lets the three cards stretch across the whole row instead of leaving two empty columns at the end, which is what its sibling keyword, auto-fill, would do.

One grid rule, three widths
Phone, about 375 px
One 16 rem column fits, so the three cards stack.
Tablet, about 700 px
Two columns fit; the third card starts a second row.
Laptop, 832 px and up
Three columns: the cards sit side by side.

One refinement you will meet in other people's code guards against a container narrower than the minimum itself. Writing the minimum as min(16rem, 100%) lets a column shrink below 16 rem when the whole grid is narrower than that, so a very small screen never scrolls sideways.

Container Queries

A media query asks about the window. That is the wrong question for a component that can appear in different places. A card in a narrow sidebar and the same card in a wide main column share one window, but they have very different amounts of room. A container query asks the element the card lives in instead. Container queries have been available in every major browser since 2023 and counted as widely available since 2025.

Fragment: index.html
<div class="card-holder">
  <article class="card">
    <h2>Opening times</h2>
    <p>Monday to Friday, nine until five.</p>
  </article>
</div>
css/style.css
.card-holder {
  container-type: inline-size;
}

@container (min-width: 30rem) {
  .card {
    display: flex;
    gap: 1rem;
  }
}

The first rule marks the holder as a container: an element whose width its descendants can ask about. The second is written like a media query, but with @container: when the nearest container is at least 30 rem wide, the card inside becomes a flex row. Put one copy of this holder in a narrow 12 rem sidebar and another in a wide column beside it. In the sidebar, the card's heading sits above its text, as usual. In the wide column, the heading and the text sit side by side. Same window, same CSS, two shapes.

The query always styles what is inside the container, never the container itself, which is why the card needs a holder around it. The holder's own width must come from the layout around it, as a block or a grid cell gets its width: a container ignores its own content when sizing itself, so a holder that would normally shrink to fit its content collapses to zero width.

Two kinds of question
Media query: asks the window
How wide is the browser window? The answer is the same for every element on the page.
Container query: asks the box it lives in
How wide is the space this component was given? A sidebar and a main column get different answers.
Common Confusions
  • "More breakpoints make a page more responsive." Fewer, better-chosen rules usually adapt better. A fluid size or an auto-fitting grid is right at every width, not only at the breakpoints.
  • "Sizing text in vw is the modern way." On its own it ignores zoom, so readers who enlarge text cannot. Combine it with rem inside clamp().
  • "Media queries and container queries do the same thing." A media query asks about the window; a container query asks about the space a component was given. The second is what makes a component reusable in different places.
Why It Matters
  • The site's card grid now works at every width with one line and no breakpoint to maintain.
  • Components that respond to their container can be dropped into a sidebar, a main column or a footer and adapt without new CSS.

Knowledge Check

A heading has font-size: clamp(1.5rem, 1rem + 2vw, 3rem). What happens on a very wide screen?

  • It keeps growing with the window, without limit
  • It stops growing at 3rem, the value at the end
  • It drops back to 1.5rem, the value at the start
  • It stays at the middle value, whatever the screen

What does repeat(auto-fit, minmax(16rem, 1fr)) do?

  • It always makes exactly sixteen equal columns
  • It limits the grid to one row of 16rem cards
  • It makes as many 16rem-or-wider columns as fit
  • It adds a breakpoint to the page every 16rem

Why is font-size: 5vw on its own a problem for some readers?

  • Zooming in does not make the text any bigger
  • Only some browsers understand the vw unit
  • It makes the text change as the reader types
  • The text becomes too large on small phones

What does a container query ask about?

  • The width of the whole browser window
  • The model of phone the page is shown on
  • The size of the stylesheet that holds it
  • The width of the element around the component

You got correct