Docs
Container Queries

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". The cq channel answers "how wide is my slot". Reach for cq whenever 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 — viewportMedias is the viewport sibling of cq: the same object-per-breakpoint grammar, triggered by @media instead of @container. It replaces the flat sm_/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.

  1. containment marks an element as a size container — the slot everything inside it will measure against.
  2. cq declares 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:

KeyMin width
xs240px
sm360px
md480px
lg640px
xl900px
// 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 cq reference alongside the viewport medias/prefix channels.
  • Server-Side Rendering — why cq is SSR-inert (the server emits static rule text; the browser resolves it).