Container Query Units for Proportional Motion
Part of Container Query Motion Triggers in Modern View Transitions & Scroll APIs.
The problem
A card component slides its detail panel in from the right when expanded. In a narrow sidebar the panel travels 24px and looks right. The same card in a wide main column still travels 24px, which barely registers across a 700px panel. Switching to translate: 100% 0 makes the panel travel its own width, which is too much in the wide layout and exposes an empty gap during the slide. Viewport units make it worse: vw distances are huge in a sidebar on a desktop and tiny in a full-width card on a phone.
Motion distance should relate to the space the component actually has — its container.
Root cause analysis: which box a length is relative to
Pixels ignore context. A fixed distance looks proportionally larger in small containers and smaller in large ones.
Percentages in translate refer to the element itself. translate: 50% 0 moves the element by half of its own width, which only equals “half the container” if the element fills the container.
Viewport units refer to the viewport. 10vw is the same in a sidebar and a hero, which is exactly what a reusable component should not depend on.
Container query units refer to the nearest query container. 1cqi is 1% of the inline size of the nearest ancestor with a container-type; cqb uses the block size; cqmin and cqmax use the smaller or larger of the two. A panel that slides by 30cqi travels 30% of its component’s width wherever the component is placed.
Fallback when there is no container. If no ancestor establishes a suitable query container, container units fall back to the small viewport units (svi, svb). The motion still works but reverts to viewport-relative behaviour — a silent bug if the container type was forgotten.
cqb needs a block container. container-type: inline-size makes only the inline axis queryable, so cqb falls back up the tree or to the viewport. Use cqi for most motion, or container-type: size when block size is also defined.
Step-by-step resolution
Production code pattern
.card {
container-type: inline-size;
container-name: card;
}
.card__detail {
--travel: clamp(16px, 8cqi, 64px); /* 8% of the card's width, bounded */
opacity: 0;
translate: var(--travel) 0;
transition:
opacity var(--motion-exit-duration) var(--motion-exit-easing),
translate var(--motion-exit-duration) var(--motion-exit-easing);
}
.card[data-expanded] .card__detail {
opacity: 1;
translate: 0 0;
transition:
opacity var(--motion-enter-duration) var(--motion-enter-easing),
translate var(--motion-enter-duration) var(--motion-enter-easing);
}
/* Keyframes read container units per animating element too. */
.card__badge {
animation: nudge-in var(--motion-enter-duration) var(--motion-enter-easing) backwards;
}
@keyframes nudge-in {
from { opacity: 0; translate: 0 clamp(4px, 3cqi, 12px); }
}
@media (prefers-reduced-motion: reduce) {
.card__detail { --travel: 0px; }
.card__badge { animation: none; }
}
Rendering Impact: composite. Container units are resolved to pixels when styles are computed; the animation interpolates plain lengths on
translate. Resizing the container during an animation retargets future transitions but does not add per-frame layout.
Resolving --travel inside the component, rather than passing a pixel value from outside, keeps the component self-contained: the same markup works in a sidebar, a modal and a full-width layout. Using a custom property also gives the reduced-motion branch one value to neutralise.
Duration should not follow container size linearly
It is tempting to scale duration with the same container units, so wider components animate longer. Resist doing so linearly: a card that fills a 1600px screen would animate several times longer than one in a sidebar, and wide layouts would feel slow. If duration must vary, use a small tier — for example, a slightly longer token above a container-size breakpoint via @container card (inline-size > 720px) — or the sub-linear scaling described in scaling duration by distance and size. Clamped distances already keep speed within a sensible range.
Verification checklist
Constraints and trade-offs
container-typecreates size containment on the chosen axis, which can change layout if the element relied on its contents for width.- Nested containers make units refer to the nearest one; name containers and check which applies.
- Container units in unsupported browsers invalidate the declaration; provide a pixel fallback declaration first.
- Very large containers can push clamped maxima into view as the dominant case; choose maxima for the widest realistic placement.
cqboften falls back unexpectedly withinline-sizecontainers.
Frequently asked questions
What does 1cqi mean?
One percent of the inline size of the nearest ancestor query container. With no suitable container, it falls back to the small viewport inline unit.
Why not use translate percentages?
Percentages in translate refer to the element’s own size, not its container, so they only match container proportions when the element fills its container.
Should animation duration use container units too?
Not linearly. Keep durations on the token scale, or use small tiers or sub-linear scaling, so wide layouts do not feel slow.
Why does my cqb distance ignore the container?
container-type: inline-size only exposes the inline axis. cqb then resolves against another container or the viewport. Use cqi or container-type: size.
Related
- Container Query Motion Triggers — the parent topic
- Combining Container Queries with Motion States — switching motion by container size
- Parameterising Keyframes with Custom Properties — reusing keyframes with contextual distances