Choosing container-type: inline-size, size, or normal
The narrow problem
You added container-type to a wrapper, wrote a @container block, and something went wrong — either the query silently never matched, or worse, the element you made a container flattened to nothing and its children spilled across the page. Both outcomes trace back to a single decision made in one declaration: which of the three container-type values you picked. The values are not three flavours of the same thing. Each applies a different amount of CSS containment, and containment is a promise to the layout engine that changes how the box is sized. This page explains exactly what each value contains, why one of them can zero out an element's height, and the rule of thumb that keeps you out of trouble. It sits inside Container Query Syntax Basics, part of Mastering Container Queries & Responsive Layouts.
Why the choice is forced on you at all
A query container has to answer a question about its own size before its descendants are laid out. That creates a circularity risk: if a descendant's styles can change the container's size, and the container's size decides those styles, the engine could loop forever. CSS resolves this by requiring containment. When you declare a container type, you are promising that the contained axes can be sized without consulting the subtree, which breaks the loop and lets the engine evaluate the query once, cheaply, in a single layout pass.
That is the whole reason inline-size and size differ. The inline axis of a block-level box is already independent of its contents in normal flow — a <div> is as wide as its parent regardless of the text inside it — so containing that one axis costs nothing and forbids nothing. The block axis is the opposite: a box's height is normally the sum of its children. Containing it means the engine deliberately stops asking the children how tall they are, and you must supply the height some other way.
The alternative to all of this is JavaScript with a ResizeObserver, which does work and predates container queries. It also runs after layout, can produce a visible reflow on first paint, and needs teardown logic per component. The declarative form has none of those costs, so the price of understanding containment is worth paying.
What each value contains
container-type: normal is the initial value. It applies no size containment whatsoever and establishes the element as a query container for style queries only. Size conditions such as (min-width: 30rem) will never match against it, because the element has made no promise that would let the engine answer them safely.
container-type: inline-size applies inline-axis size containment plus layout, style, and paint containment. The box's inline size is taken from its own formatting context rather than its contents; its block size continues to be computed from its children exactly as before. Every width-based condition resolves, and so do the inline container units described in Container Query Units cqi and cqb.
container-type: size applies size containment on both axes. That unlocks (min-height: …), (aspect-ratio: …) and block-axis conditions, at the cost of the engine no longer deriving the box's height from its contents at all.
The failure mode: a container that renders at zero height
This is the trap, and it is worth stating plainly. Under container-type: size, an element whose height is auto has no source of block size left. The children are excluded by containment, so the used height is 0. The border collapses to a hairline, backgrounds vanish, and the children paint outside the box's own area — usually straight over whatever follows.
The live demo below puts the two side by side. Drag the resize handle and watch the inline-size panel behave normally while the size panel stays flat no matter how much text is inside it.
Here is the same thing as a self-contained document you can save and open.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>container-type: size collapse</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 2rem; max-width: 40rem; }
.panel { margin-bottom: 2.5rem; }
.panel__tag { margin: 0 0 0.4rem; font: 0.75rem ui-monospace, monospace; color: #666; }
.box {
border: 2px dashed #7aa2ff;
background: #7aa2ff14;
padding: 0.75rem;
}
.box__text { margin: 0; }
/* Inline axis contained only. Height is still the sum of the children,
so this box looks and behaves like an ordinary div. */
.box--inline { container-type: inline-size; }
/* Both axes contained. Nothing supplies a block size, so the used
height is 0 and .box__text paints outside its own container. */
.box--size { container-type: size; }
/* This resolves against .box--inline. It cannot resolve against a
container whose type is normal. */
@container (max-width: 24rem) {
.box__text { font-style: italic; }
}
/* The repair: give the size container a determinate block size. */
.box--size-fixed {
container-type: size;
block-size: 8rem; /* any determinate height works */
}
</style>
</head>
<body>
<section class="panel">
<p class="panel__tag">container-type: inline-size</p>
<div class="box box--inline">
<p class="box__text">Height still grows with this text, and a width query still fires.</p>
</div>
</section>
<section class="panel">
<p class="panel__tag">container-type: size (no height set)</p>
<div class="box box--size">
<p class="box__text">Height is now zero, so this text escapes the dashed border entirely.</p>
</div>
</section>
<section class="panel">
<p class="panel__tag">container-type: size with block-size: 8rem</p>
<div class="box box--size-fixed">
<p class="box__text">A determinate block size restores the box and makes height queries meaningful.</p>
</div>
</section>
</body>
</html>
Key technique: what counts as a determinate block size
"Set a height" is the short version, but the engine accepts several sources, and knowing them means you rarely need a hardcoded pixel value:
- An explicit
heightorblock-sizein any absolute or relative unit. - An
aspect-ratiocombined with a resolved inline size — the technique covered in aspect-ratio for Responsive Media. - Being a grid item stretched into a track whose size the parent grid already knows, or a flex item stretched by
align-items: stretchin a row with a resolved cross size. - Absolute positioning with both
topandbottomset.
If none of these apply, size is the wrong value for that element. The corollary is the rule you should internalise: reach for size only when the query you want to write is genuinely about the block axis, and only on an element that already has a height for an independent reason. A media tile with a fixed aspect ratio, a viewport-height hero, a stretched dashboard cell — those are the legitimate cases. A card, a form section, a sidebar, a list item: inline-size, every time.
Variation: scoping the choice with named containers
Setting container-type on a broad selector is convenient and dangerous, because containment applies to every element the selector matched. Pair the type with a name so that queries target the right ancestor, and keep the type as narrow as the query needs. Naming is covered at length in Nesting and Naming Container Queries; the point here is that the name and the type are independent decisions.
/* Every card is a width container. */
.card {
container-type: inline-size;
container-name: card;
}
/* One specific element also needs block-axis queries — and it has a
height already, because aspect-ratio gives it one. */
.card__media {
container-type: size;
container-name: media;
aspect-ratio: 16 / 9; /* the determinate block size that makes `size` safe */
}
@container card (min-width: 30rem) {
.card { grid-template-columns: 12rem 1fr; }
}
@container media (min-height: 12rem) {
.card__caption { display: block; }
}
/* Opt one variant back out of containment entirely. */
.card--bare { container-type: normal; }
The shorthand container: card / inline-size sets both properties at once and is worth preferring in production for exactly the reason above: it makes it impossible to name a container and forget its type, or vice versa.
Browser support note
Size container queries and all three container-type values shipped together and have been supported since Chrome 105, Edge 105, Safari 16, and Firefox 110, making them baseline across evergreen browsers since 2023. There is no partial-support situation to code around — a browser either understands container-type or ignores the declaration and the @container block along with it. If you must serve older engines, gate the enhancement with @supports (container-type: inline-size) as described in Feature Detection with @supports.
FAQ
Which container-type should I use by default?
Use inline-size. It contains only the inline axis, leaves height driven by content, and supports every width-based query, which covers almost every component layout you will write.
Why did my element disappear after I set container-type: size? Size containment tells the engine to size the box without looking at its contents. With no explicit height the block size resolves to zero, so the box renders as a flat line and its children overflow it.
Can I query a container's height at all?
Only with container-type: size, and only when that container has a determinate block size from a height, aspect-ratio, or a stretched grid or flex track. Otherwise a height query has nothing meaningful to resolve against.
Does container-type: normal do anything useful? Yes. It applies no size containment but still establishes a query container for style queries, and it is the value you set to opt a specific element back out of a container rule inherited from a broad selector.
Related
- Container Query Syntax Basics — the parent guide to
@containersyntax. - Nesting and Naming Container Queries — pairing a type with a name and resolving the nearest container.
- Container vs Media Queries Comparison — when the viewport is still the right thing to ask.
- Preventing Flex and Grid Overflow — the other sizing rule that silently breaks components.
- Motion That Scales with Container Size — cross-area: driving animation from the container you just declared.
Related articles
More pages in the same section.