Skip to main content

February 19, 2026

How to Use scrollIntoView() in Webflow (With Sticky Header Offset and Smooth Easing)

5 min read

Scroll into view thumbnailscro

You click a nav link. The page scrolls. And your section disappears behind the sticky header.

If you've built anything in Webflow with anchor links and a fixed or sticky nav, you've hit this. Webflow's built-in anchor links work fine for simple pages. But the second you add a sticky header, things break. The section scrolls to the very top of the viewport, and your nav sits right on top of it.

There's no setting in Webflow to fix this. No offset option. No "scroll a little less" toggle.

The fix is JavaScript's scrollIntoView() concept, combined with a custom scroll function that accounts for your header height. And the good news is you only need one script to handle it all. Same-page links, cross-page links, smooth animation, and precise positioning.

Let's set it up.

The Webflow Drop-In Script

This is the production-ready version. Copy this, paste it into your Webflow project, and you're done.

It handles same-page nav links, cross-page hash scrolling, sticky header offset, configurable easing, per-section scroll padding, and scroll cancellation when the user interrupts. It uses a capture-phase click handler that runs before Webflow's own scroll handler, so the two never fight each other.

Where to put it

Go to Project Settings → Custom Code → Footer Code and paste the entire block below. This runs it on every page. If you only need it on specific pages, use a page-level Embed element placed before </body> instead.

The full script

<script>
const SCROLL_SETTINGS = { duration: 1500, easing: 'easeInOutCubic' };

const EASING_FUNCTIONS = {
  linear: t => t,
  easeInQuad: t => t * t,
  easeOutQuad: t => t * (2 - t),
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  easeInCubic: t => t * t * t,
  easeOutCubic: t => (--t) * t * t + 1,
  easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};

let scrollAnimationId = null;
let scrollStartTime = 0;

function smoothScrollTo(targetPos) {
  if (scrollAnimationId) cancelAnimationFrame(scrollAnimationId);
  scrollStartTime = performance.now();
  const start = window.scrollY;
  const distance = targetPos - start;
  let startTime = null;
  function step(timestamp) {
    if (!startTime) startTime = timestamp;
    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / SCROLL_SETTINGS.duration, 1);
    const easeFn = EASING_FUNCTIONS[SCROLL_SETTINGS.easing] || EASING_FUNCTIONS.linear;
    window.scrollTo(0, start + distance * easeFn(progress));
    if (progress < 1) {
      scrollAnimationId = requestAnimationFrame(step);
    } else {
      scrollAnimationId = null;
    }
  }
  scrollAnimationId = requestAnimationFrame(step);
}

function cancelScroll() {
  if (performance.now() - scrollStartTime < 100) return;
  if (scrollAnimationId) {
    cancelAnimationFrame(scrollAnimationId);
    scrollAnimationId = null;
  }
}
window.addEventListener('wheel', cancelScroll, { passive: true });
window.addEventListener('touchmove', cancelScroll, { passive: true });
window.addEventListener('keydown', (e) => {
  const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '];
  if (scrollKeys.includes(e.key)) cancelScroll();
});

function scrollToSection(targetId) {
  const target = document.getElementById(targetId);
  if (!target) return;
  const header = document.querySelector('.nav_component');
  const headerOffset = header ? header.offsetHeight : 80;
  const customPadding = parseInt(target.getAttribute('data-scroll-padding') || '0', 10);
  const elementPosition = target.getBoundingClientRect().top + window.scrollY;
  const offsetPosition = elementPosition - headerOffset - customPadding;
  smoothScrollTo(offsetPosition);
}

// Capture-phase delegated handler: runs before Webflow's, matches ANY hash link
// (including "/#events"), and only intercepts when the section is on this page.
document.addEventListener('click', function (e) {
  const link = e.target.closest('a[href*="#"]');
  if (!link) return;
  const href = link.getAttribute('href');
  if (!href) return;
  const hashIndex = href.indexOf('#');
  if (hashIndex === -1) return;
  const targetId = href.slice(hashIndex + 1);
  if (!targetId) return;
  if (!document.getElementById(targetId)) return; // not on this page — let browser navigate
  const linkPath = href.slice(0, hashIndex);
  if (linkPath && linkPath !== '/' && linkPath !== window.location.pathname) return;

  e.preventDefault();
  e.stopPropagation();
  history.pushState(null, '', `#${targetId}`);
  scrollToSection(targetId);
}, true);

window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
  const hash = window.location.hash.substring(1);
  if (!hash) return;
  window.scrollTo(0, 0);
  if (document.readyState === 'complete') {
    requestAnimationFrame(() => scrollToSection(hash));
  } else {
    window.addEventListener('load', () => {
      requestAnimationFrame(() => scrollToSection(hash));
    });
  }
});
</script>

How to Set It Up in Webflow

The setup takes about two minutes. Here's what you need to do.

  1. Add section IDs. Select each target section in the Webflow Designer and give it a unique ID in the Element Settings panel. Something like services, about, or contact.
  2. Set your nav link hrefs. Point each navigation link to the matching section ID. So if your section ID is services, the link href is #services.
  3. For cross-page links, use the full path plus the hash. For example: /about#team.
  4. Paste the script into Project Settings → Custom Code → Footer Code.

That's it. The script handles everything else.

What to Customize

You'll probably want to tweak a few things to match your project.

  • SCROLL_SETTINGS.duration controls how long the scroll animation takes in milliseconds. Default is 1500. Lower is faster, higher is slower.
  • SCROLL_SETTINGS.easing sets the animation curve. Pick from: linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic.
  • .nav_component is the CSS class for your sticky nav. Replace this with whatever class your navbar uses.
  • 80 (the fallback offset) only kicks in if the script can't find your header element. Set it to your header's pixel height as a safety net.
  • data-scroll-padding is an optional custom attribute you can add to any section for extra offset. More on this below.

Per-Section Scroll Padding With data-scroll-padding

Sometimes the header offset alone isn't enough. Maybe you've got a section with a tall top margin or a decorative element that needs more breathing room. The data-scroll-padding attribute lets you add extra pixels of space on a per-section basis.

How to add it in Webflow:

  1. Select the section element in the Webflow Designer.
  2. Open Element Settings (the gear icon).
  3. Scroll down to Custom Attributes.
  4. Add a new attribute with the name data-scroll-padding and set the value to the number of extra pixels you want (like 40).

What the values look like in practice:

  • data-scroll-padding="0" gives you no extra offset. This is the default.
  • data-scroll-padding="40" lands the section 40px below the header.
  • data-scroll-padding="100" gives you 100px of space below the header. Good for sections with large top margins or decorative elements.

If you don't add the attribute at all, it defaults to 0. So your existing sections keep working without any changes.

Fair question. Webflow's built-in anchor links (linking to #about-us, for example) work for basic pages. But they fall apart when you need precision.

  • Sticky headers cover your content. The section scrolls right behind the nav and there's no way to offset it.
  • No alignment control. Everything snaps to the very top of the viewport.
  • Smooth scroll is inconsistent. Browser defaults vary, and you can't control the easing or duration.

scrollIntoView() gives you full control over where the section lands and how it gets there. And the custom requestAnimationFrame approach in this script takes it a step further with configurable easing curves and automatic scroll cancellation.

The Basics of scrollIntoView()

Before we go further, here's a quick look at the native API this is all built on.

element.scrollIntoView({
  behavior: "smooth",  // "smooth" | "instant" | "auto"
  block: "start",      // "start" | "center" | "end" | "nearest"
  inline: "nearest"    // "start" | "center" | "end" | "nearest"
});

Quick breakdown:

  • behavior controls the animation. "smooth" gives you a polished scroll.
  • block sets the vertical alignment, meaning where the section lands in the viewport.
  • inline handles horizontal alignment. Useful for sliders or overflow containers.

The script in this post doesn't use the native scrollIntoView() directly. Instead, it builds a custom scroll function with requestAnimationFrame so you get configurable easing, header offset support, and scroll cancellation. But understanding the native API helps you see what we're replacing and why.

Things to Watch Out For

A few gotchas worth knowing about before you ship this.

  • The capture phase does the work. The handler runs in the capture phase and calls e.stopPropagation(), which stops Webflow's own scroll handler before it can fire. This runs before Webflow's handler regardless of script load order, and it doesn't depend on jQuery at all.
  • history.pushState doesn't trigger hashchange. If you have other scripts listening for the hashchange event, they won't fire when this script updates the URL hash. Probably won't matter in most Webflow projects, but it's worth knowing if you have custom hash-based routing.
  • The 100ms cancellation grace window. The script ignores wheel and touch events for the first 100ms after a scroll starts. This keeps residual trackpad or touch momentum from killing the animation the instant it begins, while still letting a real user scroll interrupt it after that beat.
  • window.Webflow.push() is undocumented. It works reliably and is widely used in the Webflow custom code community. But it's not an officially supported API. Webflow hasn't changed how it works in years, but there's always a small risk with undocumented methods.
  • Scroll cancellation is thorough. The script listens for mouse wheel, touch, and keyboard input. If the user scrolls mid-animation, it stops immediately. The keyboard listener only fires on scroll-related keys (arrows, Page Up/Down, Home, End, spacebar) so it won't interfere with form inputs.

That's the whole setup. One script, full control over your scroll behavior, and no more sections hiding behind sticky headers. Grab the script above, drop it into your Webflow footer code, and you're done.

marketing health check clipboard showing score

Your Marketing Isn't Bringing in Leads and You Can't Figure Out Why

Score your marketing across 7 key areas in 5 minutes and find out exactly what's holding back your leads and what to fix first.

Questions You Might Be Asking

Webflow scrolls the target section to the very top of the viewport, and it gives you no offset option. So with a fixed or sticky nav, the section lands directly behind the header. There's no built-in setting to fix it — you need a script that subtracts your header's height from the scroll position.

At runtime it measures your nav element (the .nav_component selector) and subtracts its height from the scroll target, then adds any per-section data-scroll-padding you've set. Swap the selector for your own navbar class, and set the fallback pixel value to your header height in case the element isn't found.

Yes. For cross-page links, use the full path plus the hash — for example /about#team. On load, the script reads the URL hash and scrolls to the matching section with the same header offset, so the section still lands in the right place after the page navigates.

Yes. Edit SCROLL_SETTINGS.duration (in milliseconds) to make it faster or slower, and SCROLL_SETTINGS.easing to pick the animation curve — options range from linear to several quad and cubic curves. For extra space on a specific section, add a data-scroll-padding attribute with the number of pixels you want.

Stop losing deals to a website that's holding you back.

Book a 30-minute strategy session. You'll leave with a clearer picture of what your site needs to do and a rough budget range, whether you hire us or not.

background cta image collage of websites

Other posts in this category

A three-way comparison graphic: three website mockups side by side labeled DIY Builder, Template, and Custom, each with a small price tag and a "best when" note beneath it, and an arrow underneath showing the progression as a business grows.

Is a Custom Website Worth It, or Should I Use a Template or DIY Builder?

Custom, template, or DIY builder? Here's how the three options compare, plus a simple decision framework.

confetti over Vimeo Video playing

How to Add a Confetti Animation When a Vimeo Video Starts Playing

Learn how to trigger a canvas confetti animation when a Vimeo video starts playing using the Vimeo Player API play event

vimeo logo against a blue background

How to Use the Vimeo Player API to Control Video on Your Website

Learn how to use the Vimeo Player API to control embedded video with JavaScript. This guide covers setup, play/pause, promises, events, and common pitfalls.