CSS container queries let a rule depend on the size or the style of an ancestor rather than of the whole screen. How to use them can be learned at MDN Docs.
An element becomes a query container with container-type, and can be given a name with container-name, or both at once with the container shorthand. A @container rule then applies its declarations to elements that have such an ancestor, when the condition holds for it.
.card {
container: card / inline-size;
}
@container card (min-width: 400px) {
.card .title {
font-size: 24px;
}
}
Container types
| Value | Description |
|---|---|
normal | The default. The element is not a size container, but can still answer style queries. |
inline-size | Queries on the width can read this element. |
size | Queries on the width, the height, the aspect ratio and the orientation can read this element. |
scroll-state | scroll-state() queries can read this element. Combines with any of the above, as in container-type: inline-size scroll-state. |
The size read is the content box, without padding and border. Unlike a browser, ReactUnity does not apply size containment: a container’s own size may still depend on its contents. That is what lets container-type: size be applied freely, and it also means a layout in which a container’s size depends on what its query changes may not settle; such a layout is left where the second pass lands.
A query is answered by the nearest ancestor that is a container of a kind able to answer it. A (height ...) query skips an inline-size container and keeps looking upwards. A query with a name only considers ancestors whose container-name includes it. An element with no eligible ancestor never matches.
Size queries
Features: width, height, inline-size, block-size, aspect-ratio and orientation. Both the min-/max- prefixed form and the range form are accepted, and conditions combine with and, or, not and parentheses.
@container (min-width: 400px) { ... }
@container (width > 400px) { ... }
@container (400px <= width < 800px) { ... }
@container sidebar (orientation: portrait) { ... }
@container (aspect-ratio > 16/9) and (height >= 200px) { ... }
@container not (width < 300px) { ... }
A length in a condition is resolved against the container, so 20em is twenty times the container’s font size.
Style queries
style() compares a property of the container. A custom property compares as the text it was declared with, and style(--theme) alone holds when the property is set to anything. A regular property compares its computed value with the given one, and so does a custom property registered with @property under a typed syntax such as <length> or <color>: with --gap registered as a length, style(--gap: 20px) holds for a container that declared --gap: 1em at a 20px font size.
.panel {
--theme: dark;
}
@container style(--theme: dark) {
.panel .title {
color: white;
}
}
@container style(--theme) and (width > 300px) { ... }
Every element can answer a style query, whatever its container-type, so an unnamed style query reads the parent. Shorthand properties such as border cannot be queried; query one of the properties they set instead.
Scroll-state queries
scroll-state() reads a container-type: scroll-state ancestor, a <scroll> as a rule, and
scrollable asks which edges it can still scroll towards. A value names an edge (top, right,
bottom, left, or the logical block-start, block-end, inline-start, inline-end), an
axis (x, y, inline, block), or none for a container with nothing left to scroll; with no
value at all it holds for any edge. Features combine with and, or and not inside the
parentheses, and the rule is evaluated again as the container scrolls or its content resizes.
scroll {
container-type: scroll-state;
}
@container scroll-state(scrollable: bottom) {
scroll .more-below { opacity: 1; }
}
@container scroll-state(not (scrollable: top)) {
scroll .header { box-shadow: none; }
}
stuck takes the same values and asks which edges of its scrollport are currently holding a
position: sticky box. It is read off the container itself rather than an ancestor, so the sticky
element is the one that carries container-type: scroll-state. As always, the rules inside style
what the container contains and never the container itself, so the visible change belongs on a
child.
.header {
position: sticky;
top: 0;
container-type: scroll-state;
}
@container scroll-state(stuck: top) {
.header .title { font-size: 14px; }
}
snapped parses and never holds — nothing snaps yet.
Container units
cqw, cqh, cqi, cqb, cqmin and cqmax are a percentage of the nearest size container’s content box on that axis. cqi and cqb mean the same as cqw and cqh. Each axis looks for its own container: an inline-size container answers cqw, while cqh comes from the nearest size container above. Without any container, the units fall back to the viewport, the way vw and vh behave.
.card {
container-type: inline-size;
}
.card .title {
font-size: 5cqw;
padding: 2cqmin;
}
Nesting
A @container rule can be nested in a style rule, in a @media or @supports block, in a @layer, and in another @container. Nested container queries must all hold.
.title {
font-size: 16px;
@container card (min-width: 400px) {
font-size: 24px;
}
}
Notes
- In UGUI, a container that changes size is re-queried in the same frame, right after layout. In UIToolkit, where layout belongs to the framework, the change is seen a frame later.
- A style query compares an unregistered custom property’s declared text after collapsing whitespace, so
style(--gap: 4px)does not match a value written ascalc(2px * 2). Registering the property with a typed@propertysyntax makes the comparison one of computed values. - A container is measured only while some rule or unit reads it. Once nothing does, it stops being checked after layout.