A breakpoint asks about the window, which is the wrong question for a card: the same card is wide in a page and narrow in a sidebar, and the viewport says nothing about which. cq asks the element's own container instead. It nests exactly like a breakpoint — the same value shape, the same states, themes and groups inside it — and it fills the same one slot, because a rule lives in exactly one at-rule block.
<Flex container> {/* container-type: inline-size */}
<Box cq={{ md: { d: 'row' } }} /> {/* @container (min-width: 28rem) */}
</Flex>
<Flex container="sidebar"> {/* a named container */}
<Box cq={{ 'sidebar/md': { d: 'row' } }} />
</Flex>The card below is written once. The left slot is 256px wide (width is the ÷4 scale, so 64) and the right one takes whatever the page has left, and the card lays itself out from that — the window never comes into it, so this reads the same in a phone-width column as it does here.
<Flex container width={64}> {/* 256px — below the sm container size */}
<Flex
d="column"
cq={{ sm: { d: 'row', ai: 'center' } }}
gap={4}
p={4}
b={1}
borderColor="slate-300"
borderRadius={3}
>
<Box width={12} height={12} borderRadius={12} bgColor="indigo-500" />
<Box flexGrow={1}>
<Box fontWeight={600}>Container queries</Box>
<Box fontSize={13} color="slate-500">
This card becomes a row once its container passes 24rem.
</Box>
</Box>
<Button variant="secondary">Open</Button>
</Flex>
</Flex>The same card again, in a container the browser lets you resize. Nothing here listens for anything: the styles are two CSS rules and the browser re-evaluates them as the box changes width. One thing a drag does need, though: every Box transitions on --transitionTime, so a resized box would chase the pointer instead of following it — and since a custom property is inherited, vars={{ transitionTime: '0s' }} on the container settles that for the whole subtree.
<Box
container
resize="horizontal"
overflow="auto"
vars={{ transitionTime: '0s' }} // direct manipulation: follow the pointer, do not animate to it
width={72}
minWidth={50}
maxWidth="fit"
p={2}
>
{/* the card from above */}
</Box>The scale is Tailwind's @xs…@2xl, so a component copied from there queries at the same widths — and deliberately far smaller than the breakpoints, because a card is 400px wide and a viewport is not. Every size has a max form, which is its complement rather than a max-width an epsilon below it: md and maxMd can never both match.
xs 20rem / 320px maxXs not (min-width: 20rem)
sm 24rem / 384px maxSm not (min-width: 24rem)
md 28rem / 448px maxMd not (min-width: 28rem)
lg 32rem / 512px maxLg not (min-width: 32rem)
xl 36rem / 576px maxXl not (min-width: 36rem)
xxl 42rem / 672px maxXxl not (min-width: 42rem)cq queries the nearest container by default, which is what you want until a card sits inside a card. A name is written on the container and addressed as name/size. Because that name lands in an at-rule prelude, it is validated first: anything that is not a CSS identifier — or that is a word the prelude itself uses, like not — drops the whole block, with no rule and no class name, the way an unmatched prop value does.
<Flex container="page" d="column" gap={4}>
<Flex container="card" p={4}>
<Box
cq={{
md: { fontSize: 16 }, // the nearest container — the card
'page/xl': { fontSize: 18 }, // the one two levels up
maxSm: { display: 'none' }, // and the card again, when it is narrow
}}
/>
</Flex>
</Flex>A container query is a more local statement than a breakpoint, so it is ranked after every breakpoint and before every user preference — a wide window is no reason to override what the card's own space says, and neither of them is a reason to override motionReduce. Sizes ascend; the max keys descend, so the narrower one wins where two overlap.
/* p={2} md={{ p: 4 }} cq={{ md: { p: 6 } }} motionReduce={{ p: 8 }} */
.a { padding: 0.5rem }
@media (min-width: 768px) { .b { padding: 1rem } }
@container (min-width: 28rem) { .c { padding: 1.5rem } }
@media (prefers-reduced-motion: reduce) { .d { padding: 2rem } }container means container-type: inline-size: the element's width no longer depends on its contents, so an inline-size container is laid out first and its children second. That is the point — and the reason not to declare it on everything. containerType="size" queries both axes and needs the element to have a block size of its own; containerName is the longhand, for when you want a name without the shorthand's type.
<Flex d="column" gap={4}>
<Box container /> {/* container-type: inline-size */}
<Box container="sidebar" /> {/* container: sidebar / inline-size */}
<Box containerName="panel" containerType="size" /> {/* both axes, named */}
<Box container="sidebar" containerType="normal" /> {/* and off again */}
</Flex>A rule sits in one at-rule block, so cq does not nest inside a breakpoint and a breakpoint does not nest inside cq — the types refuse both, the way they already refuse a breakpoint inside a breakpoint. Everything else nests in either direction: pseudo-classes, groups, themes, the state variants, a pseudo-element and startingStyle.
<Box
cq={{
md: {
hover: { color: 'indigo-500' },
theme: { dark: { bgColor: 'slate-800' } },
dataAttr: { 'state=open': { height: 40 } },
before: { content: 'Wide' },
startingStyle: { opacity: 0 },
},
}}
/>