
🔧 jQuery: smooth scroll to anchor when navigating from another page
You added an anchor to the page, put #section in the URL, and expect a nice smooth scroll. The user clicks from another page, the browser dutifully applies the hash... and jerks abruptly to the target. No scroll. Just an instant jump.
The problem is that browsers handle the hash instantly by default, before the DOM is fully ready and scripts have a chance to intercept the event. When a user arrives via an external link, your click handler never fires at all: there was no click event, just a direct URL navigation.
We've compiled two battle-tested jQuery solutions: a basic script for simple cases and an advanced version with click handling, page load support, and conflict avoidance. Both are tested on real projects and work with current jQuery versions.
💡 Quick overview:
- Save the hash from the URL to a temporary variable
- Clear
location.hashso the browser doesn't jump abruptly - Scroll to the anchor via
animate()with the desired offset - Restore the hash for a correct URL in the address bar
Why smooth anchor scrolling matters
When a visitor follows a link like site.com/page/#pricing, they expect to see the pricing block, not the top of the page. An abrupt jump is disorienting: the user doesn't understand where they landed or what came before. Smooth scrolling provides context: the eyes catch intermediate sections, and the brain maps the route from origin to destination.
With the native CSS property scroll-behavior: smooth, things are simple: add it to your stylesheet, and the browser scrolls to the anchor automatically. But this approach breaks the moment you need a custom offset for a fixed header. A 110-pixel offset keeps the block from sliding under the header and positions it exactly where the user expects. The jQuery solution gives you that control.
There's also cross-browser compatibility. scroll-behavior: smooth doesn't work in Internet Explorer (yes, such projects still exist), while the jQuery animate() method behaves identically everywhere.
Basic script: minimal code, maximum benefit
The first option is compact, just 10 lines. It solves exactly one problem: smoothly scrolling to an anchor when the user arrives from another page or via a direct link with a hash.
1 jQuery(document).ready(function ($) { 2 var myHash = location.hash; // save the hash 3 location.hash = ''; // clear it — browser stops jerking 4 if (myHash[1] != undefined) { // hash not empty? 5 $('html, body').animate( 6 { scrollTop: $(myHash).offset().top - 110 }, 7 700 // duration in milliseconds 8 ); 9 location.hash = myHash; // restore the hash 10 } 11 });
Step-by-step breakdown, because three lines work magic, but understanding the mechanics matters:
var myHash = location.hash: read the hash from the address bar (for example,#pricing). At this point the browser hasn't scrolled yet.location.hash = '': the key trick. Clearing the hash makes the browser lose its target for the instant jump. Without this line the script would "fight" the browser's built-in behavior and lose.$(myHash).offset().top - 110: calculate the anchor's position from the top of the document and subtract 110 pixels. This is your offset for a fixed header. If your header isposition: fixedand 80px tall, use 80 (or 90 for some breathing room). If there's no header, remove the subtraction entirely.$('html, body').animate(...): scroll smoothly over 700 milliseconds. The'html, body'selector isn't a whim: different browsers scroll eitherhtmlorbody, so we specify both for reliability.location.hash = myHash: restore the hash. Now the address bar shows#pricingagain, and the page is already parked nicely at the target block.
Insert the script into your theme's functions.php, a custom JS file, or via a plugin like Code Snippets. The key requirement is that it must be inside a jQuery(document).ready() block.
Advanced version: click handling, page load, and conflict protection
The basic script covers most scenarios, but sometimes location.hash = '' doesn't work in time: the browser jumps before the hash is cleared. In those cases, use the second version. It's heavier but more reliable.
1 jQuery(document).ready(function () { 2 jQuery(window).bind('load', function () { 3 jQuery('a:not(.spu-clickable)[href*="#"]:not([href="#"])').click(function () { 4 if ( 5 location.pathname.replace(/^\//, '') == this.pathname.replace(/^\//, '') || 6 location.hostname == this.hostname 7 ) { 8 var target = jQuery(this.hash); 9 target = target.length ? target : jQuery('[name=' + this.hash.slice(1) + ']'); 10 if (target.length) { 11 jQuery('html, body').animate( 12 { scrollTop: target.offset().top - 37 }, 13 1000 14 ); 15 return false; 16 } 17 } 18 }); 19 }); 20 }); 21 22 jQuery(window).load(function () { 23 function goToByScroll(id) { 24 jQuery('html, body').animate( 25 { scrollTop: jQuery('#' + id).offset().top - 38 }, 26 1000 27 ); 28 } 29 if (window.location.hash != '') { 30 goToByScroll(window.location.hash.substr(1)); 31 } 32 });
Here's what's happening, block by block:
Block one: a click handler for internal anchor links. jQuery(window).bind('load', ...) ensures all page elements (including images and iframes) are loaded before binding the handler. The filter a:not(.spu-clickable) excludes links already handled by a popup plugin (SPU, WordPress PopUp), preventing animation conflicts. Add your own exclusion classes following the same pattern.
Block two: handling navigation from an external page. jQuery(window).load() fires later than document.ready and guarantees all elements, including the anchor, are in the DOM. The goToByScroll(id) function does exactly what the basic script does, but is called only after the window fully loads. substr(1) strips the # character from the hash.
Important note: jQuery(window).load() has been deprecated since jQuery 3.0. If your project uses a current jQuery version, replace it with:
1 jQuery(window).on('load', function () { 2 // code here 3 });
The syntax differs, but the behavior is identical.
Fine-tuning: offsets, selectors, and gotchas
The script works, but every layout is unique. Here are three common tweaks that saved real projects.
Problem one: offset().top returns incorrect coordinates. This happens when parent elements have the CSS properties transform, filter, or will-change, which create a new positioning context. offset() then calculates relative to that context instead of the document. Solution: use offset({top: -0}):
1 jQuery('html, body').animate( 2 { scrollTop: jQuery('#' + id).offset({ top: -0 }).top }, 3 1000 4 );
The {top: -0} parameter forces jQuery to recalculate the position from the document boundaries, ignoring intermediate contexts. It looks like magic, but it works.
Problem two: the selector 'html, body' scrolls the wrong element. In some themes the scroll container isn't html or body but a specific block, for example, body.home for the home page or .main-content for inner pages. Identify your container via DevTools (Elements tab → search for overflow: scroll or overflow: auto) and substitute it in the selector:
1 jQuery('body.home').animate( 2 { scrollTop: target.offset().top - 37 }, 3 1000 4 );
Problem three: the hash contains Cyrillic or special characters. location.hash returns a URL-encoded sequence instead of a readable string, and the jQuery selector $(myHash) won't find the element. Solution: decode the hash with decodeURIComponent() before using it in the selector.
Video: live example of scrolling from an external link
A short demo from John Smith: smooth anchor scrolling via jQuery animate() on a real page with a fixed header and external navigation.
⁉️🤔 Frequently asked questions
The script works on the same page but not when navigating from another URL. Why?
Because the
clickhandler never fires: there was no click. The user followed a direct link, and the browser processed the hash before your JS ran. The fix is the sequencelocation.hash = ''+animate()+location.hash = myHash. Clearing the hash cancels the built-in jump, the animation scrolls smoothly, and restoring the hash preserves the correct URL in the address bar.
What offset should I use for a fixed header?
The exact header height plus a few pixels of breathing room. Open DevTools (F12), select your site's
headerelement, and check itsheightin the Computed tab. For example, if the header is 80px, use 90 in your code. If the mobile version has a different header height, add a media query with a dynamic offset calculation.
Is jQuery still relevant in 2026?
Yes, especially within the WordPress ecosystem. jQuery remains in the WP core, many plugins and themes depend on it, and migrating the entire ecosystem to native JS will take years. For new projects in plain JavaScript, smooth scrolling is done via
element.scrollIntoView({ behavior: 'smooth' })orscroll-behavior: smoothin CSS, with significantly fewer lines of code. But if you're modifying an existing WP site, the jQuery solution is still reliable and appropriate.
Can I avoid jQuery altogether?
You can and should, if the project is new. The native equivalent of the basic script:
1 document.addEventListener('DOMContentLoaded', () => { 2 const hash = window.location.hash; 3 if (hash) { 4 window.location.hash = ''; 5 const target = document.querySelector(hash); 6 if (target) { 7 window.scrollTo({ 8 top: target.getBoundingClientRect().top + window.pageYOffset - 110, 9 behavior: 'smooth' 10 }); 11 } 12 window.location.hash = hash; 13 } 14 });
Same principle: save the hash, clear it, scroll, restore it. The difference is that
behavior: 'smooth'uses the browser's native CSS engine rather than a jQuery timer. You can't configure the speed, but the code is 30 KB lighter (the weight of jQuery).
Why $('html, body') instead of just $('html')?
Firefox scrolls
html, Chrome usesbody, and Safari depends on the version. Specifying both selectors inanimate()guarantees it works across all browsers: jQuery sends the animation to whichever element actually applies it.
Is jQuery worth using just for smooth scrolling in 2026
If you have a WordPress site or a legacy project where jQuery is already loaded, grab the script from this article and don't overcomplicate things. Ten lines of code that work everywhere, in any browser, with no polyfills required. For new builds in plain JavaScript, use native scroll-behavior: smooth or scrollIntoView(). Both approaches produce smooth scrolling, but the jQuery method gives you control over speed, offset, and the scroll container selector. Native implementations require separate checks to achieve the same.
The main rule we've learned from dozens of projects: always clear the hash before scrolling. Without this trick no handler can beat the browser's built-in behavior, and users will keep experiencing abrupt jumps.



