Writing · CSS

Every way to make a layout responsive

Media queries, container queries, fluid type, flexbox, grid and the units under them: each one played across a width you drag, and when to reach for which. 9 minutes, 6 demos.

Try it: one layout, every width
example.com/cards820px

tablet: 2 columns

cards.csscss
1.cards {
2 display: grid;
3 grid-template-columns: 1fr;
4}
5@media (min-width: 640px) {
6 .cards { grid-template-columns: repeat(2, 1fr); }
7}
8@media (min-width: 1024px) {
9 .cards { grid-template-columns: repeat(3, 1fr); }
10}
On this page

01 · The idea

One layout, every width

A page doesn’t get to choose the screen it lands on. The same markup has to read well on a phone held in one hand, a tablet on its side and a laptop with the window dragged half open. “Responsive” is just the name for that: a layout that changes with the room it’s given, instead of one drawn for a single width and squeezed to fit the rest.

CSS has more than one way to do it, and they answer different questions. Some ask how wide the screen is, some how wide the box is, and some don’t ask at all: they let the content push and give. Most good layouts use a few of them together, each where it fits. This piece plays them one at a time, then says when to reach for which.

02 · Media queries

Steps the screen decides

The oldest way, and still the most used. A media query wraps some CSS in a condition about the screen, usually its width, and the browser applies it only while the condition holds. The widths where the layout changes are called breakpoints.

cards.css
.cards { display: grid; grid-template-columns: 1fr; }

@media (min-width: 640px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .cards { grid-template-columns: repeat(3, 1fr); }
}

That’s the demo at the top of the page: one column, then two at 640px, then three at 1024px. Write the smallest screen first and add to it as the screen grows (min-width), and each step only has to say what changes.

Media queries are right for the page’s frame: how many columns the page has, whether the navigation folds into a menu, how much margin there is round everything. They’re the wrong tool for a component that might sit in a wide column on one page and a narrow sidebar on the next: it only knows the screen, not where it was put.

03 · Container queries

The component reads its box

A container query asks the same kind of question about a box instead of the screen. Mark an element as a container, and anything inside it can change with the container’s width. Below, the same card sits in a sidebar and in the main column of one page, and the screen never changes width. Drag the line between them: each card stacks while its column is under 20rem and lays out side by side once it’s wider, so the two can disagree. A media query couldn’t do that: it would give both the same answer.

Container queries: the card reads its box
example.com/articles0px

.sidebar

0px < 20rem

.main

0px < 20rem

sidebar: stacked · main: stacked

card.csscss
1.sidebar, .main {
2 container-type: inline-size;
3}
4.card {
5 display: flex;
6 flex-direction: column;
7}
8@container (min-width: 20rem) {
9 .card { flex-direction: row; }
10}

Now switch container-type off. Both cards stop responding, because a container query only reads an element marked as a container; without that line there’s nothing to ask, and the query never matches. It’s the first thing to check when one doesn’t work.

This is what makes a component portable. The same card can go in a sidebar, a grid of three or a full-width hero, and it fits each without the page telling it anything. Inside a container, cqw is 1% of its width, the way vw is 1% of the screen’s.

04 · Fluid type

A size that follows the width

Steps are fine for columns, but type that jumps from one size to the next at a breakpoint can look clumsy: a heading that’s comfortable at 1023px is suddenly much bigger at 1024. clamp() lets a size grow smoothly with the screen instead, between a floor and a ceiling.

Fluid type: a size that follows the width
example.com/notes900px

Notes

A heading that follows the width

Drag the window's edge: the heading grows with the screen, never under its floor or over its ceiling.

Read on

900px wide → font-size: 44.8px

type.csscss
1/* fluid: following the width */
2h1 {
3 font-size: clamp(2rem, 1rem + 3.2vw, 3.5rem);
4}
5/* stepped: a size per breakpoint */
6h1 { font-size: 2rem; }
7@media (min-width: 640px) { h1 { font-size: 2.75rem; } }
8@media (min-width: 1024px) { h1 { font-size: 3.5rem; } }

Read clamp(2rem, 1rem + 3.2vw, 3.5rem) as three parts, and only one of them sets the size at a time: never smaller than 2rem (the floor, up to a 500px screen), never bigger than 3.5rem (the ceiling, from 1250px), and in between, 1rem plus 3.2% of the screen’s width. The graph above the window draws exactly that, flat at each end, and the code marks the part in charge as you drag. The page in the window is laid out at the screen’s real width and shrunk to fit, so the heading takes the room it really would. Switch to the steps to see the same sizes reached with media queries, in jumps.

05 · Flexbox

A row that bends

Flexbox lays things out in a line and decides how they share it. It needs no breakpoints at all: give the items a starting size and let them wrap onto a new line when they run out of room.

Flexbox: a row that bends
example.com/tags0px
DesignMotionTypeLayoutCode
row.csscss
1.row {
2 display: flex;
3 flex-wrap: wrap;
4 gap: 0.5rem;
5}
6.chip {
7 flex: 0 1 6rem; /* grow shrink basis */
8}

Each item’s flex is three numbers. Grow: whether it takes a share of the space left over. Shrink: whether it gives some up when there isn’t enough. Basis: the size it starts from, 6rem here. With wrapping on, an item that can’t fit moves to the next line; with it off, they all shrink to stay on one, and the words get cut. Switch grow on and the items on each line stretch to fill it.

Flexbox suits one direction at a time: a row of buttons, tags, a toolbar, a card’s image beside its text.

06 · Grid

Tracks that fill the row

Grid works in two directions at once, rows and columns. One line of it makes a whole responsive gallery, again with no breakpoints: as many columns as fit, none narrower than a minimum, all sharing what’s left.

Grid: auto-fit or auto-fill
example.com/gallery0px
1
2
3

1 track, stretched to fill

gallery.csscss
1.gallery {
2 display: grid;
3 grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
4 gap: 8px;
5}
gallery.css
.gallery {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}

minmax(110px, 1fr) says a column is at least 110px and otherwise an equal share. auto-fit and auto-fill both make as many as fit, and differ only when there are fewer items than room: auto-fit lets the empty columns collapse so the items stretch across, auto-fill keeps them, drawn in ember in the demo, so the items stay their size. Use fit when a short row should fill the space, fill when items should look the same however many there are.

07 · The units

The pieces underneath

All of these lean on a handful of units and functions. Most of the time the right size is one the content gives, not one you type:

Intrinsic sizes: as wide as the content says
example.com/sizes0px

width: min-content

Every way to make a layout responsive

width: max-content

Every way to make a layout responsive

width: fit-content

Every way to make a layout responsive
sizes.csscss
1.a { width: min-content; } /* its longest word */
2.b { width: max-content; } /* all on one line, overflowing */
3.c { width: fit-content; } /* squeezed: as wide as the box */
  • min-content is as narrow as the content can go: its longest word. max-content is its width with no wrapping at all, even if that runs out of the box. fit-content is max-content until the box is too small, then it gives way.
  • vw / vh are 1% of the screen’s width and height. On phones, vh ignores the browser’s toolbars, so a full-height section can hide under them; dvh is the height that’s really there.
  • cqw is 1% of the nearest container’s width, the container query version of vw.
  • min() / max(): width: min(100%, 40rem) is a measure that’s never wider than 40rem and never overflows its box, in one line.
  • aspect-ratio keeps a box’s shape as its width changes (aspect-ratio: 16 / 9 for a video), with no padding hacks.

08 · Choosing

When to reach for which

  • The page’s frame (columns, navigation, margins): media queries, smallest screen first, breakpoints where the content breaks.
  • A component that goes in different places: container queries, so it fits its box and not the screen.
  • Type and spacing that should grow with the screen: clamp(), with a rem in the middle.
  • Things in a row (buttons, tags, toolbars): flexbox with wrapping, and no breakpoint at all.
  • A grid of cards or images: repeat(auto-fit, minmax(..., 1fr)), again no breakpoint.
  • Before any of these: let the content size itself (min-content, fit-content, min()), and add a rule only where it breaks.