CSS layout reference

Components that respond to their own space.

The same component ends up in a sidebar, a modal, and a full-width column on one screen, and a media query cannot tell them apart. This reference covers container queries, the units that come with them, fluid values with clamp(), and where media queries still belong.

@container

Container queries

A media query asks how wide the screen is. A component rarely cares: the same card is narrow in a sidebar and wide in the main column on the same screen. A container query asks how wide the component’s own container is. Declare an ancestor as a container with container-type, then write @container rules for the things inside it.

Quarterly report

Stacked below 18rem of container, side by side above it.

The window has not changed size, so no media query could do this. The card is asking about the dashed box.

Use it for

  • Cards, media objects, and form rows reused across columns of different widths.
  • Dashboard widgets that the user can resize or rearrange.
  • Design-system components, which cannot know what layout they will be dropped into.
  • Anything inside a resizable panel, where the viewport never changes but the space does.

Watch for

  • An element cannot query itself. The container is an ancestor, and the rules apply to its descendants — so a component usually needs a wrapper to be the container.
  • container-type: inline-size applies size containment on the inline axis: the container’s width can no longer depend on its content. On a shrink-to-fit element — a float, an inline-block, an auto-sized flex item — it collapses to zero.
  • Do not make every element a container “just in case”. Containment has a cost, and unnamed queries then match whichever wrapper happens to be nearest.
  • Prefer inline-size over size. size also contains the height, so the container needs an explicit one or collapses.
.card-slot { container-type: inline-size; }

.card { display: grid; gap: 0.75rem; }

@container (min-width: 22rem) {
  .card { grid-template-columns: 5rem minmax(0, 1fr); align-items: center; }
}

container-name

Named containers

An unnamed @container rule matches the nearest ancestor container, whatever it is. That is fragile: wrap the component in one more container and every query silently retargets. Naming the container pins the query to the ancestor you meant, and lets one component respond to two different containers at once.

Use it for

  • container: card / inline-size as the shorthand — name first, then type.
  • Querying an outer layout container from deep inside nested ones: @container sidebar (min-width: 20rem).
  • Libraries and design systems, where unnamed queries would collide with the host page’s containers.

Watch for

  • A query naming a container that is not an ancestor never matches, and fails silently — check the name before the condition.
  • Names are not scoped: two unrelated ancestors with the same name resolve to the nearest one.
  • In Tailwind the equivalents are @container/name on the ancestor and @md/name: on the descendant.
.sidebar { container: sidebar / inline-size; }
.widget  { container: widget / inline-size; }

@container widget  (min-width: 18rem) { .title { font-size: 1.25rem; } }
@container sidebar (max-width: 14rem) { .title { display: none; } }

cqi

Container units

Container units are to a container what vw and vh are to the viewport: 1cqi is one percent of the container’s inline size, 1cqb of its block size, with cqw, cqh, cqmin, and cqmax alongside. They let padding, gaps, and type scale smoothly with the component instead of stepping at breakpoints.

font-size

Fluid heading

Bare 8cqi scales without limit: unreadable when narrow, enormous when wide. clamp() gives it a floor and a ceiling.

Use it for

  • Headings and numerals that should fill a tile at any size.
  • Padding and gap proportional to the component: padding: 4cqi.
  • cqi rather than cqw, so the unit follows the writing mode.

Watch for

  • With no container ancestor they fall back to the small viewport units — the layout appears to work, but is tracking the screen.
  • cqb needs a container with container-type: size; under inline-size it also falls back to the viewport.
  • A bare container unit on text has no minimum, and unlike rem it ignores the user’s font-size preference. Always wrap it in clamp() with rem bounds.
.tile       { container-type: inline-size; }
.tile .stat { font-size: clamp(1.5rem, 12cqi, 4rem); }
.tile .body { padding: clamp(0.75rem, 4cqi, 2rem); }

clamp(min, preferred, max)

Fluid values with clamp()

clamp() takes a minimum, a preferred value, and a maximum, and returns the preferred value held between the two. With a viewport or container unit in the middle, one declaration replaces a stack of breakpoints: the value grows smoothly, then stops. min() and max() are the one-sided versions.

Use it for

  • Fluid type: clamp(1.75rem, 1.2rem + 2.5vw, 3rem). Adding a rem term to the middle keeps the text responsive to zoom and to the user’s font size.
  • Section padding and grid gaps that tighten on small screens.
  • width: min(100% - 2rem, 65ch) — a centred column with a gutter, in one line and no media query.
  • minmax(min(100%, 14rem), 1fr) to stop a grid’s minimum track overflowing a narrow container.

Watch for

  • A middle value made only of vw does not change when the user zooms, which fails WCAG’s resize-text requirement. Mix in rem.
  • If the minimum is larger than the maximum, the minimum wins. Easy to do by accident when the bounds are tokens.
  • Operators inside calc-like functions need spaces on both sides: 100% - 2rem, not 100%-2rem.
  • Fluid sizing removes breakpoints for size, not for structure. A layout that changes shape still needs a query.
h1      { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
section { padding-block: clamp(2rem, 8vw, 6rem); }
.column { width: min(100% - 2rem, 65ch); margin-inline: auto; }

@media

Media query or container query

Container queries do not replace media queries; they take over the half of the job media queries were never good at. Page-level structure — how many columns the shell has, whether the nav is a bar or a drawer — really is about the viewport. So are the user’s preferences and the device’s capabilities. What goes inside those regions is the component’s business.

Use it for

  • @media for the page shell, and for prefers-color-scheme, prefers-reduced-motion, prefers-contrast, hover, pointer, and print.
  • @container for everything reusable that lives inside the shell.
  • Both together: the media query rearranges the regions, and each component adapts to whatever width its region ends up with.

Watch for

  • Porting every media query to a container query makes the shell depend on containers it has no reason to declare.
  • hover and pointer media features describe the primary input, not the only one. Do not hide essential controls behind hover on the strength of them.
  • Container style queries — @container style(--variant: compact) — are a separate, newer feature with uneven support. Size queries are the safe part.
@media (min-width: 60rem) {
  .shell { grid-template-columns: 16rem minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; }
}

Start from the symptom

A card looks wrong in the sidebar but right in the main column
Container queries
Adding container-type made the element collapse
Container queries
A query started matching the wrong ancestor
Named containers
Text should scale with its tile
Container units
I have five breakpoints for one font size
Fluid values with clamp()
I am not sure which kind of query a rule belongs in
Media query or container query

Keep going

A container that is too small has to scroll.

The scroll reference covers chaining, snapping, scroll spies, scrollbar styling, and scroll locking.

Open the scroll reference

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.