How to Fix CLS from Animations on top, left, height and margin

This guide covers a CLS source that often surprises teams, within Reducing Cumulative Layout Shift (CLS) and Core Web Vitals & Measurement: animations. A carousel that slides by animating left, an accordion that opens by animating height, a notification that slides in by animating top — each changes layout on every frame, and every frame can produce a layout shift entry.

Not every animation counts. Shifts within 500ms of a discrete user input (click, tap, keypress) are excluded, and changes made with CSS transform do not generate layout shifts at all because they do not change layout. The CLS problem arises from animations that run without recent input — autoplay carousels, timed banners, scroll-triggered reveals — or that outlast the 500ms exclusion window, and that use layout properties instead of transforms.

An accordion animated with height Timeline of a height animation that starts on click and continues past the 500 millisecond input exclusion, so its later frames count as layout shifts. An accordion animated with height Animation frames excluded frames counted Shift entries ~18 small shifts 0ms 100ms 200ms 300ms 400ms 500ms 600ms 700ms 800ms 900ms 500ms exclusion ends

Rapid Diagnosis

  • Look for clusters of tiny shifts. In the Performance panel's Layout Shifts track, an animation shows as a dense series of small shifts at frame intervals rather than one large one.
  • Check hadRecentInput. Log shift entries; those during the first 500ms after a click are flagged and excluded, later ones are not.
  • Inspect the animated properties. In the Animations drawer (More tools → Animations), capture the animation and read which properties it animates.
  • Use the Rendering drawer's "Layout Shift Regions". Animated regions flashing blue repeatedly confirm layout-driven animation.
  • Check autoplay. Carousels, tickers and rotating banners animate without input; every shift they cause counts.

Root Cause Analysis

1. Layout properties change geometry. top, left, right, bottom, width, height, margin, padding and font-size all change the element's layout box. Changing the box of an element in normal flow moves its siblings — that movement is a layout shift.

2. Positioned elements still count. Animating top on an absolutely positioned element does not move siblings, but it moves the element itself, and the element's own movement is a shift if it is visible and was not just added.

3. Long or delayed animations escape the exclusion. A 300ms transition triggered by a click is excluded. The same transition with a 400ms delay, or an 800ms duration, has frames outside the window.

4. Animation without input. Autoplay and scroll-linked animations never have recent input. Scrolling is not a discrete input for this purpose, so scroll-driven reveals that animate layout properties count in full.

Which property changes produce layout shifts Classification of commonly animated CSS properties by whether they change layout, generate shift entries, and their rendering cost. Which property changes produce layout shifts Property Changes layout? Counts toward CLS? Cost per frame top / left yes, for the element yes, without input layout + paint height / width / margin yes, moves siblings yes, without input layout + paint transform no never composite only opacity no never composite only

Step-by-Step Resolution

1. Convert positional animations to transform

css
/* Before: animates layout every frame. */
.toast { position: fixed; top: -80px; transition: top 300ms; }
.toast.is-visible { top: 16px; }

/* After: final position is set once; only the composited transform animates. */
.toast { position: fixed; top: 16px; transform: translateY(-120%); transition: transform 300ms; }
.toast.is-visible { transform: translateY(0); }
/* trade-off: the element's layout box sits at its final position for the whole
   animation, so hit-testing and focus see it there early. Add
   pointer-events: none until visible if it overlaps interactive content. */

Expected outcome: zero layout-shift entries from the animation and smoother motion, since compositor-only animations do not wait on the main thread.

css
.track { display: flex; transform: translateX(calc(var(--index) * -100%)); transition: transform 400ms ease; }
.slide { flex: 0 0 100%; }
@media (prefers-reduced-motion: reduce) { .track { transition: none; } }
/* trade-off: a transform-based track keeps all slides laid out, which costs
   memory for many large slides. Virtualise beyond ~10 slides by keeping only
   neighbours in the DOM. */

Expected outcome: autoplay carousels stop contributing to CLS entirely — often the single largest CLS fix on marketing homepages.

3. Animate height changes with the FLIP technique or grid rows

Accordions genuinely change layout — content below must move. Make the movement shift-free by keeping it within the input exclusion window, or animate a visual proxy.

css
/* Animate grid-template-rows: the layout change happens, but in one step on click,
   and the visual expansion is clipped by the container. Keep duration < 500ms. */
.panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 250ms ease; }
.panel.is-open { grid-template-rows: 1fr; }
.panel > .inner { overflow: hidden; }
/* trade-off: this still animates layout — each frame is real layout work —
   but because it completes within 500ms of the click, the shifts are excluded.
   Never trigger it from a timer or scroll, where no input excuses it. */

Expected outcome: user-triggered accordions remain excluded from CLS because the animation completes within the exclusion window.

4. Make scroll-triggered reveals transform-only

Reveal-on-scroll effects should fade and translate from their final layout position, never collapse and expand.

css
.reveal { opacity: 0; transform: translateY(24px); transition: opacity 400ms, transform 400ms; }
.reveal.in-view { opacity: 1; transform: none; }
/* trade-off: content starting at opacity 0 is invisible to LCP until revealed.
   Never apply reveal effects to above-the-fold content or the LCP element. */

Expected outcome: scroll reveals no longer contribute to CLS; with modern CSS this can even be done with scroll-driven animations without JavaScript.

CLS from a marketing homepage, per animated component Bar chart of CLS contributed by three animated components before and after converting them to transform animations. CLS from a marketing homepage, per animated component Hero carousel (left) 0.12 Promo ticker (margin) 0.05 Scroll reveals (height) 0.04 All three after transform 0 CLS 0.1

Verification

Re-record with Layout Shift Regions enabled and let autoplay run for at least two cycles; no regions should flash. In the trace, the Layout Shifts track should be empty during animations, and the Main track should show no Layout events per frame — transform animations run on the compositor. In CI, a Lighthouse run with a 10-second observation will catch autoplay regressions: assert cumulative-layout-shift below 0.05.

Animation Libraries and Framework Transitions

Many animation shifts come from libraries rather than hand-written CSS, so audit their configuration.

Framer Motion and similar layout animation APIs. The layout prop animates layout changes using transforms (the FLIP technique) — it measures before and after, then animates a transform between them. The layout change itself happens in one frame, so if it is not triggered by input, that single frame is still a shift. Layout animations are shift-free only for input-triggered changes.

Vue <Transition> and <TransitionGroup>. Enter/leave transitions using height or max-height classes are common in component libraries. Replace with opacity and transform classes; for list reordering, <TransitionGroup> uses transforms for moves, which is safe.

GSAP and Web Animations API. Both can animate any property. Prefer x/y (transform) over left/top in GSAP, and transform keyframes in WAAPI. These run off the main thread when only transform and opacity are animated.

CSS-in-JS animation helpers. Generated keyframes sometimes include layout properties for convenience presets ("slideDown" animating height). Inspect the generated CSS before trusting a preset.

FAQ

Does will-change: transform prevent layout shifts?

No. will-change is a hint about compositing; it does not change which properties are animated. Animating top with will-change: transform set is still a layout animation. The fix is to animate transform itself.

Do CSS transitions on hover count toward CLS?

Hover is not a discrete input for the exclusion rule, so a hover transition that changes layout can count. In practice hover effects usually change color, shadow or transform, which do not shift layout. A hover that grows a card's height and pushes neighbours is a CLS risk — use transform: scale() instead.

What about animating font-size or letter-spacing for emphasis?

Both change text metrics and therefore layout: the text box grows, lines rewrap, and everything after it moves. Emphasis animations on headings or numbers ("count-up" statistics that change width as digits change) are a frequent hidden source of small shifts. Use transform: scale() for emphasis, and give animated numbers a fixed width with font-variant-numeric: tabular-nums so changing digits never change the box size.