20 hand-coded container query patterns — the mechanism itself, not a layout that happens to use one. Covers container-type, the cqi unit family, named containers, style queries and scroll-state, plus the reflow patterns people build and the five ways a query silently does nothing. All Pure CSS, scoped under .cq-NN, framework-agnostic.
Want to build production-ready CSS layouts, animations and UI from scratch?
You just browsed 20 container query patterns. Learn the Flexbox, Grid and animation thinking behind them, so the next component starts in your head, not in a copy-paste.
The reference implementation the rest of this collection borrows from: container-type: inline-size on the parent, an @container (min-width: 21em) block in the stylesheet, and the child rules inside it. The same card markup sits in a wide slot and a narrow slot side by side, so one query produces two layouts, and an annotated panel labels each of the three moving parts.
The whole unit family shown resolving against a container you can drag wider. cqw and cqh are physical, cqi and cqb are logical and follow writing mode, cqmin and cqmax take the smaller or larger of the two. The second panel adds what most references skip: what cqb resolves against under inline-size.
An anonymous @container resolves against the nearest ancestor that establishes one, which in a nested component is usually not the one you meant. Naming the ancestor with container-name and querying @container cq-03-page (min-width: 18em) targets it directly. Both panels nest identically, so the wrong answer and the right one sit side by side.
One card component in three placements — a wide feature slot, a mid-width grid cell and a narrow rail — with layout, image ratio and type scale all changing from container width alone. The markup and class names are byte-for-byte identical in all three; nothing about the page layout is baked into the component, which is the point that makes the whole idea click.
A content block that reacts to the space a collapsible sidebar leaves it: two columns when the rail is collapsed, one when it is expanded, decided entirely by the main region's own inline size. The subject is the content responding to space it did not choose — the sidebar is context, and the toggle is a real checkbox read with :has(), so no script is involved.
The same component wired both ways, in a layout that breaks the media-query version: at one viewport width the card looks right in the main column and wrong in the sidebar, because the viewport says nothing about the space the component actually occupies. The demo then states the honest division of labour — media queries still own page-level, print and prefers-* decisions.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
@container style(--cq-07-tone: featured) queries a custom property's value instead of a size, so one token on the parent restyles every descendant — replacing a cascade of modifier classes. The card below is driven by a single --cq-07-tone value, shown next to the BEM version it replaces, with the honest caveat that only custom properties are queryable today.
container-type: scroll-state turns three previously script-only facts into CSS: whether an element is currently stuck, whether its scroll container can still scroll, and whether a snap target is snapped. All three state families are shown annotated in one scroller, replacing the IntersectionObserver sentinel trick that was the only way to detect them for years.
Type that scales with its container instead of the window: clamp(1.05rem, 4cqi, 2.1rem) beside clamp(1.05rem, 4vw, 2.1rem) in two identical narrow cards, so the difference is visible rather than asserted. The vw version is sized by a window it knows nothing about; the cqi version is sized by the card it actually lives in.
One stat widget at three container widths: a sparkline plus delta at full width, a number and label at medium, and a bare number in the narrowest slot. The same component fills every dashboard slot, so the tile decides its own level of detail from the space it was given — no size prop, no variant class, no dashboard grid required.
Commerce cards where each cell is its own container, so a card in a two-up row and a card in a four-up row style themselves differently without the grid telling them to. The detail worth naming: the container must be the grid item, not the grid — querying the grid gives every card the same answer, which is exactly the bug this pattern is supposed to remove.
The oldest layout pattern on the web — image beside text, stacking when there is no room — finally solved at component level. Historically this needed a modifier class per context (media--stacked, media--compact); now it is one rule that answers to whatever column the object lands in. Kept deliberately plain, because the mechanism is the whole content.
The same form rendered in a page, a modal and a drawer at one viewport width: field rows sit side by side where there is room, stack where there is not, and labels move from beside the control to above it. Forms are the clearest case for container queries because a form's context changes far more often than the window does. Layout only — no validation styling, no submission.
A nav that shows its full link row while it has room and collapses to a disclosure when it does not — decided by its own width, so the same component works in a page header and inside a narrow card. The collapsed state is a native <details> element, so it is keyboard accessible and needs no script, and the links live in the markup once.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Tabular data that becomes a stack of labelled cards when its container is narrow, with the label for each value coming from ::before and a data- attribute. The accessibility tradeoff is stated rather than skipped: changing display on table elements removes the implicit table semantics, so this demo keeps the real markup and accepts the loss deliberately.
How to ship container queries so an engine that lacks them still gets a good layout: a flex-wrap baseline declared first, then @supports (container-type: inline-size) gating the enhanced path. Baseline since February 2023, so the realistic fallback target is old enterprise browsers — the baseline should be a design you would ship, not a broken one.
A container inside a container, and the resolution rule that governs it: an unnamed @container matches the nearest ancestor that establishes a context, so a nested container shadows its parent for unnamed queries below it. The diagram shows the surprise first — a query meant for the outer container answering from the inner — then the fix.
The trap with no media-query equivalent: if a container's size depends on its contents and a query changes those contents' size, the dependency is circular. That is precisely why container-type: size requires both dimensions to be determined from outside, and why inline-size is the safe default. The demo shows the collapse that results, and the rule that avoids it.
auto-fit with minmax() decides how many columns fit; a container query on each item decides what each item looks like at the width it got. They answer different questions and compose cleanly. Not a Grid tutorial — the subject is the interaction, and the trap named at the end is putting container-type on the grid instead of the items.
Five ways a container query silently does nothing, as broken-then-fixed pairs: container-type missing on the parent, container-type on the element being styled, a container with no definite inline size, an unnamed query resolving too near, and size used where inline-size was meant. Each pair is live, so the fix is visible rather than described.
FAQ
Frequently asked questions
What are CSS container queries and how do they differ from media queries?
A media query asks about the viewport. A container query asks about the space a component actually occupies, which is usually the more useful question. The distinction matters because the viewport tells you nothing about where a component sits — the same card in a full-width article and in a narrow sidebar sees the identical viewport width and, with media queries, gets the identical layout. One of those two placements is always wrong. Container queries fix that by moving the decision to the component: declare container-type: inline-size on the wrapper, write @container (min-width: 30em), and the styles inside apply based on the wrapper's width rather than the window's. Media queries are not obsolete — they still own page-level decisions, print styles, and the prefers-* feature queries — but for anything that is a reusable component, the container is the right thing to measure.
Why is my container query not working?
In the overwhelming majority of cases, container-type is missing from the parent — or worse, it has been applied to the element you are trying to style. This is the single defining rule of the feature and the easiest to get backwards: the query measures an ancestor and the styles apply to its descendants. An element can never query itself. The second most common cause is a container with no definite inline size, which gives the query nothing to measure. The third is nesting: an unnamed @container resolves against the nearest ancestor that establishes a containment context, so if a closer container exists between your query and the one you meant, the closer one answers. Naming the intended ancestor with container-name and querying it by name fixes that. The fourth is using container-type: size where inline-size was meant, which requires the block axis to be externally determined and collapses the element when it is not.
What do cqi, cqw, cqb, cqmin and cqmax mean?
They are the container query unit family, and each one is a percentage of the query container's dimensions rather than the viewport's. cqw and cqh are physical width and height. cqi and cqb are the logical equivalents — inline and block — and they follow writing mode, which is why they are the ones you almost always want in shipped code. cqmin and cqmax resolve to the smaller or larger of the inline and block values. The practical significance is that 1cqi means one percent of the nearest query container's inline size, so a component sized in these units composes correctly wherever it is placed — something vw could never do, because vw always refers to the window no matter how narrow the column is. One caution: do not size body copy in cqi alone, because it stops responding to the reader's font-size preference. Keep a rem floor via clamp().
Are container queries safe to use in production?
Yes. Container queries reached Baseline in February 2023 — Chrome 105, Safari 16 and Firefox 110 — which means every current browser has supported them for well over two years. This is not an experimental feature, and treating it cautiously is out of date. Two newer additions to the family are genuinely not Baseline and should be treated differently: style queries with @container style() ship in Chrome and Safari but not Firefox, and container-type: scroll-state is Chrome-only at present. Both should sit behind an @supports gate with a working fallback. For the core feature, the sensible pattern is to write a good single-column or flex-wrap baseline first and let the @container block enhance it — engines that lack support ignore the block entirely and keep the baseline, which should be a layout you would happily ship rather than a broken one.
Can a container query style the container itself?
No, and this is a deliberate part of the design rather than an oversight. If an element could restyle itself based on its own size, you would have a circular dependency: the style changes the size, the new size changes the query result, the new result changes the style. Browsers avoid that by scoping container queries to descendants only. The same reasoning explains why container-type: size is stricter than inline-size — querying both axes requires both to be externally determined, because a container whose height depends on its contents cannot also have its contents depend on its height. In practice this means inline-size is the right default: the inline axis is nearly always set by the parent layout, while the block axis stays intrinsic. If you find yourself wanting an element to query itself, add a wrapper and query that instead.
Do container queries replace CSS Grid or Flexbox?
No — they answer a different question, and the two compose well. Grid and Flexbox decide how items are arranged in the space available; a container query decides what each item looks like at the size it ended up. The clearest example is grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)), which works out how many columns fit, combined with a container query on each grid item, which works out whether that item shows a horizontal or stacked layout at the width it received. One trap is worth naming: put container-type on the grid items, not on the grid itself. Applying it to the grid means every item queries the same width — the grid's — which defeats the entire purpose and produces identical styling everywhere. The container has to be the thing whose size actually varies.
What are style queries and scroll-state queries?
They are two newer additions to the container query family that query something other than size. A style query — @container style(--variant: featured) — reads the computed value of a custom property on the container and styles descendants accordingly, which lets a single token on a parent drive a whole visual treatment instead of a cascade of modifier classes. Today only custom properties are queryable, not arbitrary declarations. A scroll-state query — @container scroll-state(stuck: top) — lets an element style itself when it becomes stuck, when its scroll container is scrollable, or when it is snapped, which replaces the IntersectionObserver sentinel trick that was the only way to detect a stuck sticky header for years. Support differs from the core feature: style queries are in Chrome and Safari but not Firefox, and scroll-state is Chrome-only, so both need an @supports gate.
How do container queries work in React, Vue, Svelte and Tailwind?
They need no framework support at all, which is part of the appeal — this is plain CSS, so it works identically in every framework and in no framework. The shift in thinking is that a component no longer needs to be told what context it is in. Patterns like passing a size="compact" prop, or a variant that exists only to say "you are in the sidebar", largely disappear: the component reads its own available width and decides. That removes a category of prop-drilling and a category of bugs where the prop and the actual placement disagree. Tailwind ships first-class support through the @container variant — mark an element @container and then use @md:flex-row style utilities on its children, with arbitrary values available for custom breakpoints. For plain CSS Modules, styled-components or vanilla stylesheets, write the at-rule as normal; there is nothing to configure.