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.
- 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 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.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 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.
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.