Container vs Media Query Motion
Part of Container Query Motion Triggers in Modern View Transitions & Scroll APIs.
The problem
The same component can appear in a narrow sidebar and a wide main column on one page. A motion rule keyed to the viewport (@media) treats both instances identically, even though one has a fraction of the other’s space. A rule keyed to the container (@container) lets each instance animate according to the room it actually has. Choosing the wrong axis produces motion that either fights a cramped layout or fails to scale up where there is space — the classic mismatch component-driven design was built to fix.
Root-cause analysis: two different questions about size
@media (min-width: …) asks how big is the viewport? It is a page-global condition: every element sees the same answer regardless of where it sits in the layout. That is exactly right for decisions about the whole page — suppressing a full-bleed parallax on phones, or shortening durations on small screens.
@container (min-width: …) asks how big is my containment context? You establish that context with container-type: inline-size on a wrapper, and descendants then resolve the query against that wrapper’s width. Two instances of the same component in differently sized columns get different answers, so the same CSS produces contextually appropriate motion in each. Neither query changes where interpolation runs — a transform/opacity animation inside either block is still sampled on the compositor, the same discipline as compositor-only property optimization. The query only decides which rules apply when the relevant box resizes; it costs nothing per animation frame.
Decision matrix
| The motion depends on… | Use | Example |
|---|---|---|
| The component’s own available width | @container |
A card that expands its detail panel only when its column is wide enough |
| The viewport size as a whole | @media |
Disabling a hero parallax below a phone breakpoint |
| Where a reusable component sits in this layout | @container |
A media object that slides its thumbnail in only in the wide placement |
| A global preference or capability | @media |
prefers-reduced-motion, prefers-color-scheme, coarse pointer |
| Both component space and a global preference | @container and @media |
Size-driven reveal, always suppressed under reduced motion |
| Print or a specific output medium | @media |
Freezing all motion for @media print |
The two are not rivals: container queries handle layout-contextual motion, media queries handle page- and *preference-*level motion, and reduced-motion is always a media query that overrides everything.
Production code
The card below reveals a detail row only when its own container is wide enough — regardless of viewport — and is unconditionally suppressed under reduced motion via a media query.
/* Intent: the card animates based on the space IT has, not the viewport. */
.card-wrap {
container-type: inline-size; /* establish a size query context */
container-name: card;
}
.card__detail {
opacity: 0;
transform: translateY(8px);
}
/* Component-driven motion: only reveal when the card's own box is wide. */
@container card (min-width: 360px) {
.card:hover .card__detail,
.card:focus-within .card__detail {
transition: opacity 180ms ease, transform 180ms ease;
opacity: 1;
transform: translateY(0);
}
}
/* Page-level decision: this is genuinely a viewport concern, so @media. */
@media (min-width: 1200px) {
.card__detail { /* room for a slightly larger travel on wide screens */
--reveal-distance: 12px;
}
}
/* Accessibility gate: reduced motion is ALWAYS a media query and wins. */
@media (prefers-reduced-motion: reduce) {
.card__detail,
.card:hover .card__detail,
.card:focus-within .card__detail {
transition: none;
opacity: 1; /* show the detail with no motion */
transform: none;
}
}
Rendering Impact:
opacity+transform— composite only. The@containerquery re-evaluates on container resize, not per frame; the active reveal is sampled on the compositor thread.
Verification checklist
Constraints and trade-offs
container-type: inline-sizeestablishes layout containment on the wrapper, which can change how percentage heights and intrinsic sizing resolve inside it — verify the layout after adding it.- Container queries cannot query the element that is the container, only descendants; structure a wrapper accordingly.
- Reduced-motion must remain a media query: never gate accessibility behind a container size, or narrow instances would keep animating.
- Over-nesting container contexts multiplies style recalculation on resize; keep containment to the components that need it.
- For motion that should track scroll rather than size, use a view() timeline instead of a size query.
Related
- Container Query Motion Triggers — the parent guide to size-driven motion states
- Combining Container Queries with Motion States — layering container conditions with interaction state
- Accessible Motion Architecture — why reduced-motion stays a media-query override at every breakpoint