Modifying location.hash without page scrolling

124,496

Solution 1

I think I may have found a fairly simple solution. The problem is that the hash in the URL is also an element on the page that you get scrolled to. if I just prepend some text to the hash, now it no longer references an existing element!

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash.replace("btn_","")).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash="btn_"+$(this).attr("id")
            //return false;
    });
});

Now the URL appears as page.aspx#btn_elementID which is not a real ID on the page. I just remove "btn_" and get the actual element ID

Solution 2

Use history.replaceState or history.pushState* to change the hash. This will not trigger the jump to the associated element.

Example

$(document).on('click', 'a[href^=#]', function(event) {
  event.preventDefault();
  history.pushState({}, '', this.href);
});

Demo on JSFiddle

* If you want history forward and backward support

History behaviour

If you are using history.pushState and you don't want page scrolling when the user uses the history buttons of the browser (forward/backward) check out the experimental scrollRestoration setting (Chrome 46+ only).

history.scrollRestoration = 'manual';

Browser Support

Solution 3

Step 1: You need to defuse the node ID, until the hash has been set. This is done by removing the ID off the node while the hash is being set, and then adding it back on.

hash = hash.replace( /^#/, '' );
var node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
}
document.location.hash = hash;
if ( node.length ) {
  node.attr( 'id', hash );
}

Step 2: Some browsers will trigger the scroll based on where the ID'd node was last seen so you need to help them a little. You need to add an extra div to the top of the viewport, set its ID to the hash, and then roll everything back:

hash = hash.replace( /^#/, '' );
var fx, node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
  fx = $( '<div></div>' )
          .css({
              position:'absolute',
              visibility:'hidden',
              top: $(document).scrollTop() + 'px'
          })
          .attr( 'id', hash )
          .appendTo( document.body );
}
document.location.hash = hash;
if ( node.length ) {
  fx.remove();
  node.attr( 'id', hash );
}

Step 3: Wrap it in a plugin and use that instead of writing to location.hash...

Solution 4

I was recently building a carousel which relies on window.location.hash to maintain state and made the discovery that Chrome and webkit browsers will force scrolling (even to a non visible target) with an awkward jerk when the window.onhashchange event is fired.

Even attempting to register a handler which stops propogation:

$(window).on("hashchange", function(e) { 
  e.stopPropogation(); 
  e.preventDefault(); 
});

Did nothing to stop the default browser behavior. The solution I found was using window.history.pushState to change the hash without triggering the undesirable side-effects.

 $("#buttons li a").click(function(){
    var $self, id, oldUrl;

    $self = $(this);
    id = $self.attr('id');

    $self.siblings().removeClass('selected'); // Don't re-query the DOM!
    $self.addClass('selected');

    if (window.history.pushState) {
      oldUrl = window.location.toString(); 
      // Update the address bar 
      window.history.pushState({}, '', '#' + id);
      // Trigger a custom event which mimics hashchange
      $(window).trigger('my.hashchange', [window.location.toString(), oldUrl]);
    } else {
      // Fallback for the poors browsers which do not have pushState
      window.location.hash = id;
    }

    // prevents the default action of clicking on a link.
    return false;
});

You can then listen for both the normal hashchange event and my.hashchange:

$(window).on('hashchange my.hashchange', function(e, newUrl, oldUrl){
  // @todo - do something awesome!
});

Solution 5

A snippet of your original code:

$("#buttons li a").click(function(){
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});

Change this to:

$("#buttons li a").click(function(e){
    // need to pass in "e", which is the actual click event
    e.preventDefault();
    // the preventDefault() function ... prevents the default action.
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});
Share:
124,496

Related videos on Youtube

FiniteLooper
Author by

FiniteLooper

I'm a front-end web designer and developer. I make user interfaces better, write a lot of Angular code, and I've usually got some coffee

Updated on February 01, 2020

Comments

  • FiniteLooper
    FiniteLooper over 4 years

    We've got a few pages using ajax to load in content and there's a few occasions where we need to deep link into a page. Instead of having a link to "Users" and telling people to click "settings" it's helpful to be able to link people to user.aspx#settings

    To allow people to provide us with correct links to sections (for tech support, etc.) I've got it set up to automatically modify the hash in the URL whenever a button is clicked. The only issue of course is that when this happens, it also scrolls the page to this element.

    Is there a way to disable this? Below is how I'm doing this so far.

    $(function(){
        //This emulates a click on the correct button on page load
        if(document.location.hash){
         $("#buttons li a").removeClass('selected');
         s=$(document.location.hash).addClass('selected').attr("href").replace("javascript:","");
         eval(s);
        }
    
        //Click a button to change the hash
        $("#buttons li a").click(function(){
                $("#buttons li a").removeClass('selected');
                $(this).addClass('selected');
                document.location.hash=$(this).attr("id")
                //return false;
        });
    });
    

    I had hoped the return false; would stop the page from scrolling - but it just makes the link not work at all. So that's just commented out for now so I can navigate.

    Any ideas?

  • Raph
    Raph over 14 years
    For the browsers targeted by step 2, will that step cause the page to jump to the top if its not already there?
  • Borgar
    Borgar over 14 years
    No, what is happening in step 2 is that a hidden div is created and placed at the current location of the scroll. It's the same visually as position:fixed/top:0. Thus the scrollbar is "moved" to the exact same spot it currently is on.
  • Mark Perkins
    Mark Perkins almost 14 years
    This solution indeed works well - however the line: top: $.scroll().top + 'px' Should be: top: $(window).scrollTop() + 'px'
  • djc
    djc over 13 years
    It would be useful to know which browsers need step 2 here.
  • Swader
    Swader almost 13 years
    Great solution. Most painless of the lot.
  • Ben
    Ben almost 12 years
    Thanks Borgar. It puzzles me though that you're appending the fx div before deleting the target node's id. That means there's an instant in which there are duplicate ID's in the document. Seems like a potential issue, or at least bad manners ;)
  • Borgar
    Borgar almost 12 years
    I've just updated the answer fixing the problems pointed out in the comments here.
  • joshuahedlund
    joshuahedlund almost 12 years
    Note that if you're already committed to page.aspx#elementID URLs for some reason you can reverse this technique and prepend "btn_" to all of your IDs
  • YMMD
    YMMD almost 12 years
    Thank you so much, this also helped me. But I also noticed a side effect when this is in action: I often scroll by locking the "middle mouse button" and simply moving the mouse into the direction I want to scroll. In Google Chrome this interrupts the scroll flow. Is there maybe a fancy workaround for that?
  • Jason T Featheringham
    Jason T Featheringham over 11 years
    Does not work if you are using :target pseudo-selectors in CSS.
  • Ben Saufley
    Ben Saufley over 11 years
    Step 2 is causing some jumpiness in one place for me (I'm assuming based on CSS that gets applied to the placeholder element) and I'm wondering if I can remove it - as @djc notes, it would be good to know which browsers "some browsers" refers to. It appears that removing the added fx element removes the jumpiness in IE8 and Chrome at the very least while retaining the effectiveness of the hash change. I know this answer was posted four years ago now – is "some browsers" IE6? IE7? …Opera? Is it safe to remove fx if not supporting the likes of IE6/7?
  • Borgar
    Borgar about 11 years
    The scroll jump behavior, as I recall, was not only limited to some browsers but also to the distance to the target ID. If you want to know the details, the "Ask Question" button is your friend. Also testing it. If the issue does not affect any of the browsers you support then don't waste your time with it. --- Secondly, to debug CSS getting applied to the placeholder, use a debug tool like Firebug or Chrome Developer tools. All you have to do is run the placeholder injection code and then inspect the element.
  • Jaseem
    Jaseem about 11 years
    Removing the id is messing up with all the event handlers
  • Connell
    Connell over 10 years
    Love this solution! We're using the URL hash for AJAX based navigation, prepending the IDs with a / seems logical.
  • daniel.gindi
    daniel.gindi about 10 years
    You do not need the hashchange, just scroll back immediately
  • jordanbtucker
    jordanbtucker over 9 years
    This won't work because setting the hash property will cause the page to scroll anyway.
  • A F
    A F over 9 years
    replaceState is probably the better way to go here. The difference being pushState adds an item to your history while replaceState does not.
  • HaNdTriX
    HaNdTriX over 9 years
    Thanks @AakilFernandes you are right. I just updated the answer.
  • Matijs
    Matijs about 9 years
    It's not always the body that scrolls, sometimes it's the documentElement. See this gist by Diego Perini
  • Tofandel
    Tofandel about 9 years
    The problem with this method is with Mobile Browsers you can notice that the scrolling is modified
  • user151496
    user151496 about 9 years
    any idea on ie8/9 here?
  • max
    max about 9 years
    pushState is the way to go if you want the back / forward buttons to work in the way you users may expect.
  • max
    max about 9 years
    of course my.hashchange is just an example event name
  • allicarn
    allicarn over 8 years
    @user151496 IE 8/9 do not support pushState. caniuse.com/#search=pushState
  • HaNdTriX
    HaNdTriX over 8 years
    @user151496 check out: stackoverflow.com/revisions/…
  • Igor Trindade
    Igor Trindade over 8 years
    Perfect! After five hours trying to fix the problem with the hash reload... :/ This was the only thing that worked!! Thanks!!!
  • SventoryMang
    SventoryMang over 6 years
    This doesn't seem to work work when I have the element as a jquery element in JS and call $element.trigger("click"). The event fires, but the scroll jump still happens. Also your answer doesn't have any quotes around the #, so you will get an unrecognized expression error on a[href^=]
  • Markus
    Markus about 6 years
    @Chris-Barr @Borgar Got here by reference from the faq module faq.js While the hash is changing in the url when clicking on different faq nodes, I would like to be able to enable the scroll to page of that faq when accessing it via that url in browser with hash tag, if that makes sense? Wonder if that's even possible as the faq nodes after js processing contain: <div class="faq-question-answer"> <div class="faq-question faq-dt-hide-answer faq-processed"> <span datatype="" property="dc:title"><a href="/my-test-faq" id="t50n173">FAQ question title?</a></span>
  • s3c
    s3c over 2 years
    In my case the hash is added to url, and the hash exists, but it stopped scrolling down to the section as I want it to. Ctrl + L & Enter refresh works though... I wonder why?