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.
Step-by-step resolution
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,translateandscale. 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.
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.
Related
- Container Query Motion Triggers — the parent topic
- Motion Design Systems & Tokens — where variant vocabularies come from
- Container Query vs Media Query Motion Decision Matrix — choosing a query type