Skip to content
Blog 10 min read

Why Your Website Jumps While You Scroll (and How to Fix It)

Category:
Web
Updated:
Oct 23, 2026

Ask AI about this article

A client saw small jumps while scrolling their new site. How to find the cause in Chrome DevTools and fix images, fonts, banners, headers and scripts.

Artyum Grebenyuk, founder of AGR Studio Artyum Grebenyuk Founder, AGR Studio

TLDR:

Small jumps while scrolling are usually layout shifts: an image, font, banner, sticky header or script changing size after the page has drawn. Find the moving element in the Layout shifts track of Chrome DevTools, then reserve its space in CSS or animate it with transform. If nothing moves but the page stutters, it's main-thread jank, not layout shift.

A website jumps while you scroll when something on the page changes size or position after it has been drawn: an image without reserved space, a web font swapping in, a banner pushing content down, or a script recalculating heights. If it happens after you stop scrolling, look first at lazy-loaded media, smooth-scroll and scroll-animation scripts recalculating, and the mobile browser bar resizing the viewport. Find the element that moves in Chrome DevTools, then reserve its space or move the change to transform.

An enterprise client noticed small jumps while scrolling their new site, "sometimes even after scrolling stops", and sent us a list of suspects. It was a good list. Here is each one: what it looks like, how to confirm it, and the fix.

What counts as a layout shift?

A layout shift is a visible element changing position between two frames. Google measures it as Cumulative Layout Shift (CLS). Per web.dev:

  • A good CLS is 0.1 or less, measured at the 75th percentile of page loads, mobile and desktop separately. Above 0.25 is poor.
  • Shifts less than 1 second apart are grouped into windows of up to 5 seconds. Your CLS is the worst window.
  • Shifts within 500 ms of a tap, click or key press are excluded (hadRecentInput). Scrolling doesn't count as input, so a shift while scrolling counts in full.
  • Changes made with transform don't count, because they don't move the layout.

Is it a layout shift or scroll jank?

Ask one question: did the content move, or did the scrolling stutter?

  • Layout shift: the line you were reading moves relative to the page. Something above it grew, shrank or appeared.
  • Scroll jank: the page freezes for a moment, then catches up. Nothing changed position; frames were dropped.

Jank comes from a busy main thread. In the Performance panel, long tasks (over 50 ms) get a red triangle in the Main track and the Frames track marks dropped frames. The Rendering tab adds live FPS and outlines elements with scroll-related listeners. The fixes: passive touch and wheel listeners, shorter tasks, and no layout reads (getBoundingClientRect, offsetHeight) in scroll handlers. Heavy video is another common cause; see video-heavy website performance.

How do you find what is shifting?

  1. Open the DevTools Performance panel. The live metrics view shows your local CLS and a Layout shifts log as you scroll. It replaced the Web Vitals extension, which Chrome retired in January 2025.
  2. Record a trace while you scroll. In the Layout shifts track, each shift is a purple diamond. Click one and the Summary tab lists the elements, the score and potential culprits.
  3. In the Rendering tab, tick Layout Shift Regions. Areas that shift flash purple.
  4. Or log shifts in the console:
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue;
    for (const { node, previousRect, currentRect } of entry.sources) {
      console.log(entry.value.toFixed(4), node, previousRect, currentRect);
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

The element reported is often the victim, not the cause; web.dev suggests checking the element just before it. A large downward shift usually means something was inserted. A shift of 1 or 2 pixels usually points to a web font or conflicting CSS.

Why does Lighthouse say CLS is fine when users see jumps?

Lighthouse is a lab test: by default it only captures shifts during load. Field data from the Chrome UX Report (CrUX) covers the whole visit, scrolling included. Search Console's Core Web Vitals report uses CrUX data from the last 28 days. PageSpeed Insights shows lab and field side by side. If field CLS is much worse, the shifts happen after load.

Do images, videos and embeds reserve their space?

Looks like: text drops as an image, video or iframe arrives.

Confirm: Lighthouse lists images without explicit width and height. In a trace, the shifted elements sit right under the media.

Fix: give every image and video width and height attributes. Browsers work out the aspect ratio from them before the file loads. Use aspect-ratio for embeds.

<img src="team.jpg" width="1600" height="900" alt="The team at work">
img, video { max-width: 100%; height: auto; }
.embed { aspect-ratio: 16 / 9; width: 100%; }
.embed iframe { width: 100%; height: 100%; }

When we rebuilt agr.studio, CMS images came without dimensions. We now read the size from each asset and write it into the markup.

Are web fonts reflowing your text?

Looks like: headings re-wrap a moment after load, or every line nudges by a pixel or two.

Confirm: throttle the network in DevTools and reload. Shifts that line up with font requests are font swaps.

Fix: per web.dev, font-display: swap shows text fastest but can shift when the font arrives. optional waits up to 100 ms and never swaps, so a first-time visitor may see the fallback. Preload the one or two files used above the fold (crossorigin is required, even on your own domain). Then size the fallback like the web font with size-adjust and the metric overrides.

<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>
/* Example values: compute yours from the font's metrics */
@font-face {
  font-family: "Brand Fallback";
  src: local("Arial");
  size-adjust: 97%;
  ascent-override: 90%;
  descent-override: 33%;
  line-gap-override: 0%;
}
body { font-family: "Brand", "Brand Fallback", sans-serif; }

Chrome's guide to font fallbacks explains the maths; next/font and Fontaine generate these rules for you. Safari supports size-adjust but not the three overrides yet.

Scripts that split text into lines for animation should wait for document.fonts.ready. Split earlier and the lines change when the font arrives.

Why do line heights differ between browsers?

Some font files set a flag called USE_TYPO_METRICS, which picks the set of vertical metrics used for line spacing. Browsers don't treat it the same way. In our work on agr.studio, Chrome on Windows and Android and Firefox honoured it and Safari ignored it, so the same text sat about 0.06em higher in some browsers. We pinned ascent-override, descent-override and line-gap-override on the web font's own @font-face to the font's hhea values, the ones Safari already used. The font files stay untouched, and the line boxes match everywhere.

Is something being inserted above the content?

Looks like: the page drops by the height of a bar, a second or two after load. Cookie banners, announcement bars, late embeds and ads are the usual sources.

Confirm: a large downward shift, with the inserted element just above the shifted one.

Fix: overlay banners instead of pushing content. Reserve space for late bars and ad slots, and don't collapse a slot when nothing fills it (optimize CLS).

.cookie-banner { position: fixed; inset: auto 0 0; }
.announcement-bar { min-height: 44px; }

Scroll anchoring hides some of this: the browser adjusts the scroll position when content is inserted above the viewport. Chrome and Firefox have it, and WebKit announced it for Safari 27. Older Safari still jumps, and anchoring does nothing for content in view. It's a safety net, not a fix.

Do sections change height after load?

Looks like: a slider shows every slide stacked, then snaps into one row. A component sized by JavaScript changes height at a breakpoint. On a phone, a full-height hero grows or shrinks when the browser bar hides, often just as you stop scrolling.

Confirm: throttle the CPU and reload, then resize the window. Test on a real phone.

Fix: give the server-rendered markup its final size in CSS before any script runs. Style a carousel's uninitialised state like the initialised one. Use media queries, not JavaScript, for breakpoints.

Mind the viewport units. vh currently equals lvh, the viewport with the browser bars hidden, so a 100vh hero is taller than the screen while the bars show. svh assumes the bars are showing. Both stay fixed; dvh follows the bars, and MDN warns it can resize content while the user scrolls.

.hero { min-height: 100vh; min-height: 100svh; }

If you use content-visibility: auto, add contain-intrinsic-size: auto 800px. Without a size estimate, skipped sections collapse and the scrollbar jumps (web.dev).

Should wrappers use overflow: hidden or overflow: clip?

Use clip when a wrapper only needs to cut off overflow, such as an animation creating sideways scroll. Per MDN, hidden makes the element a scroll container when content overflows. It can still scroll, by script or when focus moves inside it. Two side effects:

  • A sticky element sticks to its nearest ancestor with overflow set to hidden, scroll, auto or overlay, even if that ancestor never scrolls (MDN on position). Your sticky header or sidebar stops sticking.
  • Smooth-scroll scripts and scroll-driven animations can measure against that accidental scroll container instead of the page.

clip hides overflow without creating a scroll container, and nothing can scroll it. One caveat: focusable content cut off by clip won't scroll into view, so don't clip interactive content.

main { overflow-x: clip; }
.marquee { overflow: clip visible; }

We use the first rule on agr.studio.

Does your sticky header change height on scroll?

Looks like: the page jumps by a header's height as you pass a point. Usually the header switches to position: fixed after you scroll (it leaves the flow, so content jumps up) or shrinks from 96px to 64px.

Confirm: turn on Layout Shift Regions and scroll slowly past the point where the header changes.

Fix: make the header sticky from the first frame, with a constant layout height. Shrink or hide it with transform, which doesn't shift layout.

.site-header { position: sticky; top: 0; height: 72px; transition: transform 0.3s; }
.site-header.is-hidden { transform: translateY(-100%); }

Is a smooth-scroll library or scroll animation recalculating?

Looks like: pinned sections jump into place, animations restart from a new position, or the page settles a moment after you stop. Smooth-scroll libraries keep easing after your input ends, so anything tied to scroll position fires "after scrolling stops".

Confirm: disable the library and scroll again. If the jumps go, it's the library or its triggers.

Fix: GSAP's ScrollTrigger recalculates start and end positions on resize, load and a few other events. If content changes height in between, positions are wrong until the next refresh, and then pinned sections jump. Call ScrollTrigger.refresh() after anything changes height. On phones the browser bar changes the viewport height; GSAP's ignoreMobileResize stops small vertical resizes on touch-only devices from triggering a refresh, which GSAP says avoids those jumps. Lenis resizes itself with a ResizeObserver by default and runs on GSAP's ticker:

gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.config({ ignoreMobileResize: true });

const lenis = new Lenis();
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);

Then check what the animations change. Animate transform and opacity, not top, height, margin or letter-spacing; layout properties move the content around them. On agr.studio, a headline animation that tightened letter-spacing re-wrapped lines mid-animation, so we moved it to a transform. With the image and font fixes above, our measured CLS went from 0.19 to 0.

Do lazy-loaded images or hydration swap content in?

Looks like: content jumps exactly when you scroll into a new area, or the page reshuffles once JavaScript finishes.

Confirm: shifts in the trace line up with image requests or with the framework's hydration.

Fix: loading="lazy" defers an image until it's near the viewport. Without width and height it starts at 0×0 and expands while the user scrolls, and that shift counts (web.dev). Size every lazy image, and don't lazy-load the first viewport. Script-based lazy-loaders need placeholders with the final aspect ratio.

Hydration jumps happen when the browser renders something different from the server HTML: a client-only component, personalised content, a date. Render the same markup on both sides, or reserve the space.

Checklist: symptom, cause, fix

SymptomLikely causeFix
Text drops as media appearsImages, video or iframes without dimensionswidth and height attributes, aspect-ratio
Headings re-wrap after loadWeb font swapMatched fallback, preload, or font-display: optional
Line heights differ by browserUSE_TYPO_METRICS handled differentlyPin metrics with @font-face overrides
Page drops by a bar's heightBanner, ad or embed inserted aboveOverlay it or reserve the space
Section snaps to a new heightScript-set heights, carousel setupFinal size in CSS before scripts run
Hero resizes during mobile scroll100vh or dvh with browser barssvh or lvh
Sticky element stops stickingoverflow: hidden on an ancestoroverflow: clip
Jump when passing a scroll pointHeader changes height or positionSticky from the start, transform to hide
Jump after scrolling stopsSmooth scroll or ScrollTrigger refreshRefresh after height changes, ignoreMobileResize
Jump when entering a new areaLazy images without dimensionsSize every lazy image
Reshuffle after JavaScript loadsHydration mismatchSame markup on server and client
Stutter, nothing movesLong tasks, blocking scroll listenersPassive listeners, shorter tasks

Notes for Framer, Webflow and custom code

  • Framer: Clip is now the default Overflow setting for new layers, replacing Hidden. Layers set to Hidden, in older projects or by hand, can still break sticky elements; switch them to Clip.
  • Webflow: the Overflow property under Style panel, Size supports clip. Webflow suggests it when sticky elements misbehave with hidden.
  • Custom code: every cause above is in your hands. On Next.js, next/font generates matched fallbacks; Fontaine does the same elsewhere.

On any platform, check embeds and custom code blocks first. In our experience, that's where reserved space most often goes missing.

If your site jumps and you can't see why, send it for our free design and SEO check; speed is part of it. For ongoing fixes by one team, there's Website Partner.

FAQ

Why does my website jump after I stop scrolling?

Usually something recalculates once scrolling settles. Smooth-scroll libraries keep easing after your input ends, scroll-animation tools such as GSAP ScrollTrigger refresh their positions when the viewport resizes, and on phones the browser bar showing or hiding changes the viewport height, which resizes anything sized in vh or dvh. Lazy-loaded images that arrive without reserved space do the same. Record a trace in the Chrome DevTools Performance panel to see which element moves.

What is a good CLS score?

Google counts a Cumulative Layout Shift of 0.1 or less as good and above 0.25 as poor, measured at the 75th percentile of page loads, separately for mobile and desktop. CLS is the worst burst of shifts on the page: shifts less than a second apart, grouped into windows of up to 5 seconds. Check your real-user score in Search Console's Core Web Vitals report or PageSpeed Insights.

Do layout shifts while scrolling count toward CLS?

Yes. Only shifts within 500 ms of a discrete input, such as a tap, click or key press, are excluded. Scrolling, dragging and pinch-zoom don't count as input, so an image or banner that pushes content while someone scrolls adds to CLS in full.

Why does Lighthouse show a good CLS when users still see the page jump?

Lighthouse is a lab test that only captures shifts during the initial page load. Real-user data from the Chrome UX Report covers the whole visit, including shifts caused by scrolling, lazy loading and late scripts. If field CLS in PageSpeed Insights or Search Console is much worse than the lab score, the shifts happen after load.

Should I use overflow: hidden or overflow: clip?

Use overflow: clip when you only want to cut off overflowing content. overflow: hidden turns the element into a scroll container when content overflows, which can stop position: sticky working for elements inside it and confuse scroll scripts. clip creates no scroll container, but don't use it on wrappers that cut off focusable content, because that content won't scroll into view.

How do I stop web fonts from causing layout shift?

Match the fallback font to the web font with size-adjust, ascent-override, descent-override and line-gap-override on a local() fallback, and preload the one or two font files used above the fold. font-display: optional removes font-swap shifts entirely, at the cost of some first-time visitors seeing the fallback. Tools such as next/font and Fontaine generate the matched fallback for you.

Blog

Continue reading our latest blogs

Free 20-min intro call

Tell us what you’re building

Tell us about your goals and we will reply within a day with a clear scope and next steps.