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

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 also surgically removes Webflow's built-in scroll handler so the two don't 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>
// Easing options: 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
// 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
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
};
// Track the animation frame ID at the top level so it persists across calls
let scrollAnimationId = null;
// Custom smooth scroll with configurable easing
function smoothScrollTo(targetPos) {
// Cancel any in-progress scroll first
if (scrollAnimationId) cancelAnimationFrame(scrollAnimationId);
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 easeProgress = EASING_FUNCTIONS[SCROLL_SETTINGS.easing](progress);
window.scrollTo(0, start + distance * easeProgress);
if (progress < 1) {
scrollAnimationId = requestAnimationFrame(step);
} else {
scrollAnimationId = null;
}
}
scrollAnimationId = requestAnimationFrame(step);
}
// Cancel scroll if the user scrolls manually (mouse wheel, touch, or keyboard)
function cancelScroll() {
if (scrollAnimationId) {
cancelAnimationFrame(scrollAnimationId);
scrollAnimationId = null;
}
}
window.addEventListener('wheel', cancelScroll);
window.addEventListener('touchmove', cancelScroll);
window.addEventListener('keydown', (e) => {
// Only cancel on keys that cause scrolling
const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '];
if (scrollKeys.includes(e.key)) cancelScroll();
});
// Calculates the correct scroll position accounting for the sticky header height,
// then smoothly scrolls to that position
function scrollToSection(targetId) {
const target = document.getElementById(targetId);
if (!target) return;
// Get the sticky header height dynamically so offset is accurate at all breakpoints
const header = document.querySelector('.nav_component');
const headerOffset = header ? header.offsetHeight : 80;
// Optional: add data-scroll-padding="40" to any section element for extra breathing room
const customPadding = parseInt(target.getAttribute('data-scroll-padding') || '0', 10);
// Get the element's distance from the top of the document
const elementPosition = target.getBoundingClientRect().top + window.scrollY;
// Subtract header height and any custom padding so the section lands exactly where you want
const offsetPosition = elementPosition - headerOffset - customPadding;
smoothScrollTo(offsetPosition);
}
// Disable Webflow's built-in scroll handler via jQuery
// This is more surgical than stopImmediatePropagation — only removes Webflow's
// handler, leaving analytics and other click listeners intact
window.Webflow = window.Webflow || [];
window.Webflow.push(function() {
$(function() {
$(document).off('click.wf-scroll');
});
});
// Same-page nav link clicks
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener("click", function (e) {
const targetId = this.getAttribute("href").substring(1);
if (targetId) {
e.preventDefault();
// Update the URL hash without triggering a jump
history.pushState(null, null, `#${targetId}`);
// Small delay ensures any layout shifts settle before calculating position
setTimeout(() => scrollToSection(targetId), 20);
}
});
});
// Cross-page: if a user arrives from another page with a hash in the URL,
// wait for Webflow to fully initialize before scrolling to the section
window.Webflow.push(function () {
const hash = window.location.hash.substring(1);
if (hash) {
setTimeout(() => scrollToSection(hash), 100);
}
});
</script>How to Set It Up in Webflow
The setup takes about two minutes. Here's what you need to do.
- 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, orcontact. - 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. - For cross-page links, use the full path plus the hash. For example:
/about#team. - 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.durationcontrols how long the scroll animation takes in milliseconds. Default is1500. Lower is faster, higher is slower.SCROLL_SETTINGS.easingsets the animation curve. Pick from:linear,easeInQuad,easeOutQuad,easeInOutQuad,easeInCubic,easeOutCubic,easeInOutCubic..nav_componentis 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-paddingis 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:
- Select the section element in the Webflow Designer.
- Open Element Settings (the gear icon).
- Scroll down to Custom Attributes.
- Add a new attribute with the name
data-scroll-paddingand set the value to the number of extra pixels you want (like40).
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.
Why Not Just Use Webflow's Anchor Links?
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:
behaviorcontrols the animation."smooth"gives you a polished scroll.blocksets the vertical alignment, meaning where the section lands in the viewport.inlinehandles 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 jQuery dependency. The line
$(document).off('click.wf-scroll')relies on jQuery, which Webflow includes by default. If you ever disable jQuery in your Webflow project settings, this line will throw an error. You'd need to switch toe.stopImmediatePropagation()with capture mode instead. history.pushStatedoesn't triggerhashchange. If you have other scripts listening for thehashchangeevent, 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 20ms click delay. There's a small
setTimeouton click events to let browser layout shifts settle before calculating the scroll position. On fast pages with simple layouts, you can drop this to10ms or remove it entirely. It's mainly there as insurance for pages with heavy layout shifts or late-loading content. 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.
Non-Webflow Version
If you're not building in Webflow, here's a generic version that works on any site. Same functionality, just without the Webflow-specific pieces.
What's different from the Webflow version:
- No Webflow handler removal. There's no
$(document).off('click.wf-scroll')or jQuery dependency. - Uses
DOMContentLoadedinstead ofwindow.Webflow.push()for cross-page hash scrolling. - No
setTimeouton click. Scrolls immediately since there's no Webflow layout shift to account for. - Pure vanilla JavaScript. No jQuery required.
Setup is the same: add section IDs, set nav link hrefs, and for cross-page links use the full path plus hash. Drop the script before </body> or in your site-wide footer code.
<script>
// Easing options: 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
// 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
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
};
// Store the animation frame ID at the top level so it persists across calls
let scrollAnimationId = null;
// Custom smooth scroll with configurable easing
function smoothScrollTo(targetPos) {
// Cancel any in-progress scroll first
if (scrollAnimationId) cancelAnimationFrame(scrollAnimationId);
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 easeProgress = EASING_FUNCTIONS[SCROLL_SETTINGS.easing](progress);
window.scrollTo(0, start + distance * easeProgress);
if (progress < 1) {
scrollAnimationId = requestAnimationFrame(step);
} else {
scrollAnimationId = null;
}
}
scrollAnimationId = requestAnimationFrame(step);
}
// Cancel scroll if the user scrolls manually (mouse wheel, touch, or keyboard)
function cancelScroll() {
if (scrollAnimationId) {
cancelAnimationFrame(scrollAnimationId);
scrollAnimationId = null;
}
}
window.addEventListener('wheel', cancelScroll);
window.addEventListener('touchmove', cancelScroll);
window.addEventListener('keydown', (e) => {
const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '];
if (scrollKeys.includes(e.key)) cancelScroll();
});
// Calculates the correct scroll position accounting for the sticky header height
function scrollToSection(targetId) {
const target = document.getElementById(targetId);
if (!target) return;
// Change '.nav_component' to your sticky nav's class or tag
const header = document.querySelector('.nav_component');
const headerOffset = header ? header.offsetHeight : 80;
// Optional: add data-scroll-padding="40" to any element for extra breathing room
const customPadding = parseInt(target.getAttribute('data-scroll-padding') || '0', 10);
const elementPosition = target.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - headerOffset - customPadding;
smoothScrollTo(offsetPosition);
}
// Same-page nav link clicks
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener("click", function (e) {
const targetId = this.getAttribute("href").substring(1);
if (targetId) {
e.preventDefault();
// Update the URL hash without triggering a native jump
history.pushState(null, null, `#${targetId}`);
scrollToSection(targetId);
}
});
});
// Cross-page: scroll on page load if URL has a hash
document.addEventListener("DOMContentLoaded", function () {
const hash = window.location.hash.substring(1);
if (hash) {
setTimeout(() => scrollToSection(hash), 100);
}
});
</script>That's the whole setup. One script, full control over your scroll behavior, and no more sections hiding behind sticky headers. If you're building in Webflow, grab the first script and drop it in. If you're on another platform, the vanilla version at the bottom has you covered.
End to End Webflow Design and Development Services
From Web Design and SEO Optimization to Photography and Brand Strategy, we offer a range of services to cover all your digital marketing needs.

Webflow Web Design
We design custom Webflow websites that are unique, SEO optimized, and designed to convert.
Webflow Support
Get dedicated design and development support from a Webflow Professional Partner without the overhead of a full-time hire or the hassle of one-off project quotes.
Claim Your Design Spot Today
We dedicate our full attention and expertise to a select few projects each month, ensuring personalized service and results.






