Learn CSS · Lesson 10 of 11 · 10 min
Responsive, without guessing the viewport
Widely availablesince Aug 2025 Widely availablesince Jan 2023
A breakpoint is a guess about a screen you will never see. For years it was the only tool, so everything was a breakpoint. It is now the last of four, and most components never need it. A card does not care how wide the window is. It cares how wide it is.
What is actually changing?
Pick the closest one. The card it lights is the tool built for that job.
Six tools, in the order to reach for them
No query at all
Widely available.cards {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
} Goes on: the container of the repeated things.
The browser fits as many columns as there is room for and recounts whenever the space changes. It works in a sidebar, a modal and a full page without knowing which one it is in. The grid lesson takes the line apart.
What it replaced: a breakpoint for every column count.
A value that scales between two limits
Widely availableh1 {
font-size: clamp(1.75rem, 1.2rem + 2.4vw, 3.25rem);
} Goes on: the element whose size should scale.
A minimum, a preferred value that grows with the viewport, and a maximum. No steps, so no width at
which the heading suddenly looks wrong. Keep a rem term in the middle value, or the text
stops responding to the reader’s zoom. The clamp calculator
writes the middle term for you.
What it replaced: a font-size in each of three media queries.
A component that asks its own container
Widely availableDrag my corner
At 360px of my own width I go side by side. The window never changed.
.card-wrap { container-type: inline-size; }
@container (min-width: 360px) {
.card { grid-template-columns: 140px 1fr; }
} Your browser does not support this yet, so the demo above shows the fallback. Write the narrow layout as the default and let the query add the wide one. A browser without container queries keeps the stacked card, which is a complete design, not a broken one.
Goes on: container-type on a wrapper, the @container rule on what is inside it.
The same card is one column in a narrow sidebar and two in a wide article, on the same page, at the same moment. A media query cannot express that, because both cards see the same viewport. Build and test one in the container query builder.
Careful. An element cannot query itself, only an ancestor. That is why the rule needs a wrapper, and it is the usual reason a container query seems to do nothing.
Sizes measured against the container
Widely available.card-wrap { container-type: inline-size; }
.card-title { font-size: clamp(1rem, 0.8rem + 3cqi, 1.75rem); } Goes on: anything inside a container.
1cqi is one percent of the container’s inline size, the container’s answer to
vw. The fluid headline in card two is actually sized this way, which is why it follows
the box you drag and ignores the window.
The page skeleton
Widely available.page { display: grid; grid-template-areas: "main" "side"; }
@media (width >= 60rem) {
.page { grid-template-columns: 1fr 20rem;
grid-template-areas: "main side"; }
} Goes on: the few rules that arrange the page itself.
The viewport is the right thing to ask when the thing changing really is the page: whether the
sidebar sits beside the content or under it. Use rem so the breakpoint moves with the
reader’s font size, and let the content choose the number. Narrow the window until the layout looks
cramped. That width is your breakpoint, whatever phone is popular this year.
What the user asked for
Widely available@media (prefers-reduced-motion: no-preference) {
.hero { animation: rise .6s both; }
}
@media (hover: hover) and (pointer: fine) {
.card:hover { translate: 0 -4px; }
}
@media (prefers-contrast: more) {
.muted { color: inherit; }
} Goes on: motion, hover effects and low-contrast decoration.
These have nothing to do with width, and they are the media queries that matter most. Write motion
inside no-preference, so the calm version is the default and nobody has to opt out of
being made dizzy. Hover styles belong inside (hover: hover), or they stick after a tap
on a phone.
Full height on a phone
Widely available.hero {
min-height: 100vh; /* older browsers */
min-height: 100svh; /* the height with the browser bars showing */
} Goes on: a section meant to fill the screen.
On a phone 100vh is the height with the address bar hidden, so the bottom of a
full-height hero starts off the screen. svh is the small, safe height, lvh
the large one, and dvh follows the bars as they move, which makes the layout shift
while scrolling. For a hero, svh is nearly always the one.
What it replaced: a resize listener writing --vh into a custom property.
Practice
- Drag the two figures above as narrow as they go. Neither has a media query. Find, in their printed CSS, the one function that keeps each from overflowing.
- Open the container query builder, set a breakpoint, and drag the container across it. Then put the same card in two containers of different widths in your head: what would a media query have done?
- Paste a site you admire into /decode and look at its media queries under “The CSS it ships”. Count how many are about width and how many are about the user.
Check yourself
The same product card appears in a wide grid and in a narrow sidebar on one page. Which tool lays it out correctly in both?
Card three. Both cards see the same viewport, so no media query can tell them apart. Each one’s container is a different width, and that is what a container query reads.
You wrote @container (min-width: 400px) on .card, and gave .card itself container-type. Nothing happens. Why?
An element cannot query itself. Put container-type on a wrapper around the card, and
style the card from inside the query.
Why is clamp(1rem, 4vw, 2rem) a problem for accessibility, and what fixes it?
Between the limits the size depends only on the viewport, so zooming the text does nothing. Add a
rem part to the middle value, such as 0.6rem + 2vw, and zoom works again.
The mental model to keep
Ask what is changing before asking how wide the screen is. If it is the amount of room a component has, the component should find out for itself. If it is the page, ask the viewport. If it is the person, ask their preferences. Most of the breakpoints in an older stylesheet are the first kind written as the second.
Last reviewed 2026-09-19. Browser status on this page is read from the web-features dataset when the site is built, not typed.