Animating Changes at Container Breakpoints

A component built with container queries changes its styles when its container crosses a size threshold: a card goes from stacked to side-by-side, a toolbar collapses its labels, a heading steps down a size. By default those changes are instant. When the container is resized interactively — a sidebar toggles, a split pane is dragged, a dashboard widget is resized — the instant switch can look like a glitch, the component "popping" from one form to the other. Transitions can smooth that change, but only for some properties, and too much motion at a breakpoint makes resizing feel heavy. This page covers what can transition, what cannot, and how to keep breakpoint motion helpful. It belongs to Container-Aware Motion in the CSS-Only Micro-Interactions & Animations guide.

Container query changes are ordinary style changes

A transition runs whenever a property's computed value changes after the element has rendered — regardless of why it changed. A container query that starts matching changes computed values exactly as a class toggle would, so any transition declared on those properties runs. No special syntax is required; the transition simply has to be declared outside the @container block so it applies in both states.

Some properties glide, the layout snaps Left, a narrow container with a stacked card: image above text, small padding and radius. Right, a wide container with the same card side by side: image beside text, larger padding and radius. Between them, labels note that padding, gap, radius, font size and colour transition, while the switch from stacked to side-by-side happens instantly. Crossing a 28rem container breakpoint transitions: padding, radius font-size, color snaps: grid areas, direction narrow: stacked wide: side by side

The complete implementation

Drag the demo's resize handle across the breakpoint. The card's layout switches instantly, but its padding, radius, heading size and accent colour transition, so the change reads as a deliberate adjustment rather than a jump.

Live demoTransitions around a container breakpoint
Drag the frame narrower and wider across about 28rem. The layout switches instantly; padding, radius, gap, heading size and border colour glide. Drag the bottom-right corner to resize the frame in either direction.
.card-wrap {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: 0.75rem;
  padding: 0.75rem;
  border-radius: 8px;
  border: 1px solid #cbd5e1;
  background: #fff;
  /* Declared outside @container so it applies in both states. */
  transition:
    padding 200ms ease-out,
    border-radius 200ms ease-out,
    gap 200ms ease-out,
    border-color 200ms ease-out;
}

.card h3 {
  margin: 0;
  font-size: 1.1rem;
  transition: font-size 200ms ease-out;
}

@container (inline-size > 28rem) {
  .card {
    grid-template-columns: 10rem 1fr;   /* discrete: snaps */
    align-items: start;
    gap: 1.25rem;
    padding: 1.5rem;
    border-radius: 16px;
    border-color: #2563eb;
  }
  .card h3 { font-size: 1.4rem; }
}

@media (prefers-reduced-motion: reduce) {
  .card, .card h3 { transition: none; }
}

Every transitioned property here is one that changes smoothly without implying movement across the screen: spacing, corner radius, text size and colour. The structural change — going from one column to two — snaps, and that is fine. The softer properties around it absorb the visual discontinuity. Test the result by dragging slowly across the breakpoint, not just by jumping between two sizes: the slow drag is how real users encounter it, and it is where an overlong duration or a transition on the wrong property becomes obvious. If the component looks like it is catching up with the drag, shorten the duration or drop a property.

The key technique: transition the frame, not the layout

The layout switch itself is usually a keyword change — a different grid-template-columns track count, flex-direction, a different set of grid-template-areas — and those are discrete. Trying to animate them directly either does nothing or requires elaborate workarounds. Instead, pick the properties that visually "frame" the component and let them carry the transition. The eye reads the frame changing smoothly and accepts the content rearranging inside it.

Discrete versus continuous at a breakpoint Left column titled snaps lists grid-template-areas, flex-direction, display, a different number of grid tracks, and order. Right column titled can transition lists padding and gap, border-radius, font-size and line-height, colours and borders, and opacity. What a breakpoint can animate Snaps Can transition grid-template-areas flex-direction, display track count changes order padding, gap border-radius font-size, colours opacity Frame the change with the right column; let the left column switch.

Note that several of the "can transition" properties — padding, gap, font-size — trigger layout on every frame. During a 200ms transition on one component, that is acceptable. It becomes a problem if many components cross breakpoints at once, for example when a dashboard sidebar collapses and twenty widgets change size together. In that situation, keep only colour and radius transitions, which are paint-only, or none at all.

Variation: fading in content that appears at a breakpoint

Container queries often reveal extra content at larger sizes — a description, a secondary action, a metadata row — by switching it from display: none to visible. A plain transition cannot animate that, because an element with display: none has no rendered style to transition from. @starting-style provides one. It defines the values an element starts from the first time it is rendered, including when it stops being display: none:

.card__meta {
  display: none;
  opacity: 1;
  transition: opacity 200ms ease-out;
}

@container (inline-size > 28rem) {
  .card__meta { display: block; }

  @starting-style {
    .card__meta { opacity: 0; }
  }
}

When the container grows past 28rem, the metadata row appears and fades from 0 to 1. When it shrinks again, the row disappears instantly, which is usually correct for a resize: fading content out while space is being taken away tends to look like a rendering delay. If the exit should fade too, add display to the transition list with transition-behavior: allow-discrete and an opacity: 0 state for the narrow case, as described in Transitioning display With allow-discrete. Keep the fade short; the goal is to soften the appearance, not to draw attention to it.

Why breakpoint motion should be small

Two facts shape the design. First, most breakpoint crossings are never watched: a component usually renders at its final size when the page loads, and transitions do not run for an element's initial style. Second, when a crossing is watched, the user is almost always resizing something on purpose, and their attention is on the resize, not the component. Large or slow motion at that moment makes the interface feel like it is lagging behind the user's hand.

Keep transitions short — 150 to 250 milliseconds — and limited to properties that do not move content across the component. Avoid transitioning translate or scale of whole components at breakpoints; content sliding around while the user drags a divider is disorienting. And do not attempt to "animate the layout" with tricks such as transitioning huge max-height values; they are slow, imprecise and interact badly with continuous resizing.

When the layout change should animate: view transitions

Sometimes a layout change is triggered by an explicit action rather than a drag — clicking "collapse sidebar", for instance — and the whole rearrangement should animate. That is a job for a view transition: wrap the action that changes the container's size in document.startViewTransition(), and give the component a view-transition-name. The browser snapshots the old and new layouts and morphs between them on the compositor, including the discrete parts CSS transitions cannot handle. Same-Document View Transitions covers the mechanics. For continuous drags, stick with small CSS transitions; a view transition per frame of a drag is not practical.

Rapid back-and-forth crossings

When a user drags a divider slowly across a breakpoint, the container can cross it several times in quick succession, and each crossing restarts the transitions. Because transitions retarget from their current value, this produces a gentle wobble rather than a flicker, which is one more reason to prefer transitions over keyframe animations for breakpoint changes. There is no CSS hysteresis — a way to switch at 28rem going up and 27rem going down — so if a component sits at a common container width and flickers between states, adjust the breakpoint rather than the animation.

Browser support

Size container queries are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. The transition shorthand is supported in Chrome 26+, Edge 12+, Firefox 16+ and Safari 9+. Same-document view transitions are supported in Chrome and Edge 111+, Firefox 144+ and Safari 18+. prefers-reduced-motion is supported in Chrome 74+, Edge 79+, Firefox 63+ and Safari 10.1+.

FAQ

Do CSS transitions run when a container query starts matching? Yes. A container query changing a property's computed value is a style change like any other, so a transition declared on that property runs. That includes changes caused by a sidebar opening, a panel being resized, or the window being dragged.

Can I transition a change from flex-direction row to column? No. flex-direction and other keyword layout switches are discrete and snap instantly. Transition the continuous properties around the switch, such as padding, gap, radius and font size, or use a view transition to animate the layout change as a whole.

Should layout changes at breakpoints animate at all? Only subtly. Users rarely watch a component cross a breakpoint, and when they do, it is usually because they are resizing something. Short transitions on a few visual properties smooth the change; long or large motion makes resizing feel sluggish.

How do I stop breakpoint transitions from firing on page load? Transitions do not run for an element's initial style, so a component that first renders at a wide size does not animate. They only run when the size changes after the first render, which is usually the behaviour you want.

Related articles

More pages in the same section.