Style Queries for Motion Variants

Part of Container Query Motion Triggers in Modern View Transitions & Scroll APIs.

The problem

The same notification component appears in three places: a marketing page that wants bouncy, expressive entrances; an admin dashboard that wants calm, short fades; and a data-dense table view that wants no movement at all except for errors. Today the component has three modifier classes, and every page that uses it has to remember which one to add. Nested usage — a notification inside a dashboard widget embedded on the marketing page — picks up whichever class someone remembered last.

The motion variant is a property of the context, not of each instance. Container style queries let a component ask its context which variant applies.

Root cause analysis: querying computed custom properties

A container style query tests the computed value of a property on the nearest container. For custom properties, @container style(--motion-variant: calm) matches when the nearest container’s computed --motion-variant is calm.

Two details make this useful for motion.

Every element is a style container. Unlike size queries, style queries do not need container-type; by default the query is evaluated against the element’s parent, and a named container can be targeted with container-name. Because custom properties inherit, setting --motion-variant: calm on a dashboard region makes every descendant’s parent report calm — the component can query it without any class.

Nearest wins. A dashboard widget inside a marketing page that sets its own --motion-variant: calm overrides the page’s expressive for everything inside it. Context composition follows the DOM automatically.

Support is uneven. Style queries for custom properties are supported in Chromium and WebKit-based browsers but not everywhere. In unsupported browsers the @container style() blocks are ignored, so the component must have a sensible default outside any query.

It is a style-time switch. Changing --motion-variant restyles descendants once; it does not affect animations already running, and it adds no per-frame cost. The animations it selects decide their own rendering tier.

How nested contexts choose a variantCustom properties inherit, and the nearest value is what the style query sees.How nested contexts choose a variantNotification component@container style(--motion-variant: calm) matchesDashboard widget--motion-variant: calmMarketing page--motion-variant: expressive:root default--motion-variant: standard
Custom properties inherit, and the nearest value is what the style query sees.

Step-by-step resolution

Context-driven motion variantsOne property on regions, queries inside components.Context-driven motion variants1Define --motion-variant with values standard, calm, expressive and still.A small, documented vocabulary2Set a default on :root and overrides on page regions.Context decides variant3Write the component's standard motion outside any query.Works without style query support4Add @container style() blocks for calm, expressive and still.Variants selected automatically5Remove the old modifier classes.No per-instance configuration6Put the reduced-motion media query last so it overrides every variant.
One property on regions, queries inside components.

Production code pattern

:root                   { --motion-variant: standard; }
.marketing              { --motion-variant: expressive; }
.dashboard              { --motion-variant: calm; }
.data-grid              { --motion-variant: still; }

/* Default (standard) motion: applies everywhere, including unsupported browsers. */
.notice {
  animation: notice-in var(--motion-enter-duration) var(--motion-enter-easing) backwards;
}
@keyframes notice-in { from { opacity: 0; translate: 0 8px; } }

@container style(--motion-variant: expressive) {
  .notice {
    animation: notice-pop 360ms var(--ease-expressive) backwards;
  }
}
@container style(--motion-variant: calm) {
  .notice {
    animation: notice-fade var(--duration-200) linear backwards;
  }
}
@container style(--motion-variant: still) {
  .notice:not(.notice--error) { animation: none; }
  .notice--error { animation: notice-fade var(--duration-200) linear backwards; }
}

@keyframes notice-pop  { from { opacity: 0; scale: 0.9; translate: 0 12px; } }
@keyframes notice-fade { from { opacity: 0; } }

/* Last: reduced motion overrides every variant. */
@media (prefers-reduced-motion: reduce) {
  .notice { animation: notice-fade 100ms linear backwards; }
}

Rendering Impact: composite. The style query selects which keyframes apply when styles are computed; each variant animates only opacity, translate and scale. Switching a region’s variant restyles its descendants once.

Order matters because @container and @media blocks do not add specificity. The reduced-motion rule wins because it comes after the variant blocks with the same selector. If a variant block is later moved below it, reduced motion silently stops applying in that context.

Modifier classes against a style querySame visual variants; different source of truth.Modifier classes against a style querynotice--calm on each instanceEvery usage must pick a classNested contexts conflictEasy to forget in new pagesPer-instance configuration--motion-variant on regionsSet once per page or regionNearest context winsComponents need no classesContext decides
Same visual variants; different source of truth.

Detecting support and testing variants

There is no simple @supports test for container style queries. Treat them as progressive enhancement: the default motion outside any query must be acceptable on its own, and variants are refinements. To test, set --motion-variant on an element in DevTools and watch the Styles pane — matching @container style() rules appear under the element with their condition. In automated tests, render the component inside wrappers that set each variant and assert the running animation name with getAnimations(), as in inspecting running animations.

Verification checklist

Constraints and trade-offs

  • Style queries for custom properties are not supported in every browser; variants are an enhancement.
  • Querying the parent by default means a component placed directly inside a region still sees inherited values, but an intermediate element resetting the property changes the result.
  • Keyword values must match exactly; a typo silently falls back to the default.
  • Many variants multiply keyframes and testing effort; keep to three or four.
  • Variants selected by context can surprise authors who expect per-instance control; document the property on the component.

Frequently asked questions

Do style queries need container-type?

No. Every element is a style container. Size queries need container-type; style queries do not.

Can a style query read an inherited custom property?

Yes. The query evaluates the container’s computed value, which includes inherited custom properties, so setting the property on an ancestor region is enough.

What happens in browsers without style query support?

The @container style() rules are ignored and the component uses its default styles, so the default motion must be acceptable.

How do variants interact with reduced motion?

Place the reduced-motion media query after the variant rules so it overrides all of them.