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 @container query 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-size establishes 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.