Container Queries
A component should adapt to the width of its slot, not the width of the
screen. The same Card belongs in a 280px sidebar and in a 900px feed, and it
should lay itself out correctly in both — without the page telling it how wide
the viewport is. That is what container queries give Apollion, exposed as a
third responsive channel next to the viewport prefixes and Grid medias.
Mental model. Viewport prefixes (
sm_,md_…) answer "how wide is the screen". Thecqchannel answers "how wide is my slot". Reach forcqwhenever a component can appear at more than one width on the same screen — a card in a grid, a panel in a sidebar, a widget in a dashboard cell.
Viewport, object grammar. For the genuinely viewport-driven cases — page shells, where containment is barred —
viewportMediasis the viewport sibling ofcq: the same object-per-breakpoint grammar, triggered by@mediainstead of@container. It replaces the flatsm_/md_/lg_/xl_prefixes.<Flex viewportMedias={{ md: { flexDirection: 'row' }, xl: { p: 'small' } }} />.
The two pieces
Container queries need two things: a container to measure against, and the query that reads it.
containmentmarks an element as a size container — the slot everything inside it will measure against.cqdeclares per-slot-width styles on a descendant, using the same factory props you already know.
<Flex containment="inline-size">
<Card cq={{ sm: { p: 'large' }, md: { flexDirection: 'row' } }} />
</Flex>containment="inline-size" watches width only (the common case, and the
cheapest). "size" watches both axes; a string names the container
("inline-size card") so a distant descendant can target it by name.
cq is mobile-first, like everything else
The base (unprefixed) styles are the narrow-slot design; wider slots opt in
through ascending min-width container queries. This is the same discipline the
whole system follows — the mobile layout is never an override, it is the
default.
Thresholds come from theme.containerSizes, a scale separate from the
viewport breakpoints — slot widths are not viewport widths, so reusing
breakpoints.values would be a category error:
| Key | Min width |
|---|---|
xs | 240px |
sm | 360px |
md | 480px |
lg | 640px |
xl | 900px |
// Base: a stacked column (the narrow-slot design).
// From the `md` slot up: a row.
<Card cq={{ md: { flexDirection: 'row', gap: 'large' } }} />Some components are already containers
Card and UploadCard carry their own containment, so their content adapts
to the component's width with no extra wrapper. Content placed inside a Card
can use cq directly:
Narrow
The meta row stacks below the sm container size.
Meta A
Meta B
Wide
Same markup, laid out horizontally.
Meta A
Meta B
The immediate-wrapper rule
Put containment on the immediate wrapper of the adapting component — the
sidebar slot, the grid cell, the panel body. Never on a page-level shell
(GlobalStyle, a layout root, body/main): the browser would monitor the
entire page for layout shifts, throwing away the performance that containment
exists to protect. This is enforced, not just advised — an automated check
forbids containment on the layout roots and caps the number of container sites.
A container also cannot query its own width — an element sizes its children against the nearest ancestor container. So a component that must restructure itself by its own width keeps the container one level up (or on a parent that is already a container).
Where cq stops and JS begins
The cq channel is pure CSS, so it can only change presentation — spacing,
direction, visibility, sizing. When a width change must swap the DOM tree
itself (a different set of elements, a portal, a focus trap), that stays a
JavaScript branch (useMediaQuery): Modal and the page layouts keep their
structural branch on purpose. Notification likewise stays a JS flag — its
mobile chrome is round/borderWidth, and border props are excluded from every
responsive channel by design.
Graceful degradation
There is no polyfill and no @supports gate. A browser that does not understand
@container simply ignores the rules and renders the base — the mobile-first
design, which is the primary target, never a broken state. The container-query
floor sits below the browser envelope the library already ships for its color
engine, so support is guaranteed a fortiori for any supported browser.
Exploring it in Storybook
Viewport tools cannot demonstrate a slot query — the slot, not the screen, is
what changes. The shared withResizableSlot decorator drops a story into a
horizontally resizable size container: drag the handle at its bottom-right
corner to change the slot width and watch cq styles respond live, with the
viewport fixed. See Card → ResizableSlot and UploadCard → SlotAdaptive
for worked examples.
See also
- Layout Props — the full prop surface, including
a shorter
cqreference alongside the viewportmedias/prefix channels. - Server-Side Rendering — why
cqis SSR-inert (the server emits static rule text; the browser resolves it).