Avoiding Resize Loops in Queried Containers
Part of Container Query Motion Triggers in Modern View Transitions & Scroll APIs.
The problem
A sidebar collapses by animating its width from 280px to 72px. The navigation inside is a query container: below 160px it switches to icon-only mode, hiding labels. As the width passes 160px during the animation, the labels vanish, the items become narrower, and on some layouts the sidebar’s content-based width shrinks further — then, on the way back open, the reverse happens and the navigation flickers between modes for a few frames around the threshold. In another component a card’s container query adds padding at wide sizes, which makes the card wider, which keeps the query matched even after the card is dragged narrower.
Container queries were designed to prevent infinite loops, but animation can still produce oscillation and sticky states.
Root cause analysis: containment breaks cycles, not feedback
Size containment prevents true cycles. A query container with container-type: inline-size has inline-size containment: its inline size is determined without looking at its contents. So styles applied by a query inside it cannot change the container’s own inline size, and the browser cannot be forced into an infinite loop. This is a deliberate design constraint of container queries.
But ancestors are not contained. If the container’s parent sizes itself from content — a flex item with width: auto, a grid track sized auto or min-content — then styles inside the container can change the parent’s size, which changes the container’s size on the next layout. The loop is broken across frames rather than within one, and it shows up as flicker when an animation is sweeping the size near a breakpoint.
Animating the queried size re-evaluates queries every frame. A width transition on the container triggers layout and query evaluation on each frame. Near a threshold, tiny rounding differences can toggle the query on consecutive frames, especially when the query’s styles change the parent’s content size.
Queries that change the queried size are fragile. Padding, borders or gaps applied by a query to an element whose size feeds back into its own container’s parent create the sticky state: once matched, the layout keeps the size above the threshold.
The general rule from the parent topic applies: queries should trigger motion, and motion should not change the queried dimension.
Step-by-step resolution
Production code pattern
.shell {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr; /* definite track: not sized from content */
}
.shell { --sidebar-w: 280px; }
.shell[data-nav="collapsed"] { --sidebar-w: 72px; }
/* Register so the track width interpolates. */
@property --sidebar-w { syntax: "<length>"; inherits: true; initial-value: 280px; }
.shell { transition: --sidebar-w var(--motion-move-duration) var(--motion-move-easing); }
.nav {
container-type: inline-size;
container-name: nav;
}
/* Mode follows STATE, not the sweeping width: no flicker mid-transition. */
.shell[data-nav="collapsed"] .nav__label {
opacity: 0;
transition: opacity var(--duration-100) linear;
}
.shell:not([data-nav="collapsed"]) .nav__label {
opacity: 1;
transition: opacity var(--duration-200) linear var(--motion-move-duration); /* after expanding */
}
/* Size query only for layouts that are not driven by the collapse animation. */
@container nav (inline-size < 120px) {
.nav__badge { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.shell, .nav__label { transition: none; }
}
Rendering Impact: layout per frame for the sidebar width. The track width is animated deliberately and costs layout; the fix removes the extra work of queries toggling and parents re-sizing mid-sweep. For a compositor-only collapse, overlay the sidebar and animate
translateinstead.
The key change is that the labels hide based on data-nav, which flips once at the start of the collapse, rather than on a container width that the animation sweeps through. The size query that remains only hides a badge in genuinely narrow placements, and nothing it changes can affect the grid track, because the track width is a definite custom property.
Hysteresis for genuinely size-driven modes
Some modes must depend on size — a card that switches layout when a user resizes a panel by dragging. Dragging sweeps slowly across a threshold and can flicker the same way. Two techniques help. Use different thresholds for entering and leaving a mode by storing the current mode in an attribute from a ResizeObserver that only switches to compact below 300px and back to regular above 340px. Or debounce the mode switch so it applies after resizing pauses. Both move the decision out of a pure query, which is the trade-off: queries are declarative but memoryless, and hysteresis needs memory.
Verification checklist
Constraints and trade-offs
- Animating grid tracks or widths is still layout per frame; overlays with transforms are cheaper.
- Registered custom properties that inherit cost style work on descendants each frame.
- State-driven modes duplicate the breakpoint in script or markup.
ResizeObserver-based hysteresis runs after layout and can lag one frame behind.- Container size containment can change intrinsic sizing behaviour of the container itself.
Frequently asked questions
Can container queries cause infinite layout loops?
Not within a container: size containment means the container’s queried size does not depend on its contents. Feedback through uncontained ancestors can still cause flicker across frames.
Why does my component flicker when its container animates past a breakpoint?
Query evaluation toggles styles as the size sweeps through the threshold, and those styles can change an ancestor’s content size. Drive the mode from state during the animation.
Should I animate the width of a query container?
Avoid animating the queried dimension. Animate a definite parent size or use transforms on an overlay, and keep mode switches independent of the sweep.
How do I add hysteresis to a container query?
Queries have no memory, so store the mode in an attribute and switch it with different enter and exit thresholds from a ResizeObserver.
Related
- Container Query Motion Triggers — the parent topic
- Combining Container Queries with Motion States — triggering motion from queries safely
- FLIP Technique for Layout Animations — animating layout changes without per-frame layout