Smooth Scroll Offset Anchor Links with CSS

29 Comments

  1. Hey,

    Granted, I know very little about CSS or any code, but it looks like you have given us an example snipet of what you are saying is not the best way, but haven’t given the example of what you suggest. Like you say, I have seen the above in many places. I still can’t get it to work.

    1. Hi Lindsay. Sorry if I wasn’t clear, but the first method works for me (try this link) and is the one I recommend using. The second is just something that I found being posted quite a bit and its not recommended.

  2. commenting to follow

  3. Atomic

    Like Lindsay, I can’t get this to work. Refreshing the page gives different results. Sometimes scrolls too far and the offset doesn’t always apply.

    1. If you’d like to post the code you’re using here or an example URL I’d be happy to take a quick look.

  4. Andrew

    Awesome! Thanks for this Bryan 🙂

    1. No problem Andrew!

  5. Andrew

    Actually, I thought this was working but now it’s not. I have a position fixed header that has a height of 100px. I have a link within the first div following the header that has a link to a div with the id of “about” . When I click the anchor it isn’t adding the required 100px of space to offset the height of the header. Am I implementing this incorrectly?

    html {
    overflow-x: hidden;
    overflow-y: auto;
    scroll-behavior: smooth;
    }

    :target:before {
    content: “”;
    display: block;
    height: 100px;
    margin: -100px 0 0;
    }

    1. Do you have a URL I could take a look at?

      1. Andrew

        Sure. https://boydells.oxwebdevelopment.com.au/ Click on the white arrow in the first section to scroll down 🙂

      2. Hi Andrew. I’m not quite sure what you are expecting to happen… It seems to be working okay for me. Here’s a screen recording where it’s behaving as expected: https://share.getcloudapp.com/BluQkLvr

        If you’re expecting something else, let me know – you an also jump on a chat during work hours Mountain time USA here: https://cinchws.com

      3. Andrew

        Gday Bryan, thanks for your time. As currently coded, the scroll-to doesn’t scroll to the very top of the #about div including its top padding.

      4. Hey Andrew. I think to get it the way you want it to work, you’ll need to move the target link inside the current div you’re targeting. Here’s what I mean: https://share.getcloudapp.com/yAuZd0Yp

  6. Thanks for the job done, Bryan!
    Found interesting detail. If target element has top padding, this method doesn’t work. Some strange behavior, but it is what it is.
    So, to have this work, add id=”target” to the element without paddings, maybe some heading. In my case both section and heading have top padding, so I just wrapped section content with div and moved my section top padding to it. Works!

  7. jackting

    This is great!
    🙂

    But it does not work with a negative text-indent p tag.
    🙁

  8. Lee James

    I find that this simple modification of your original snippet avoids the sudden appearance of extra space above the :target.

    :root {
    scroll-behavior: smooth;
    }

    :target {
    margin: -100px 0 0;
    }

    :target::before {
    content: “”;
    display: block;
    height: 100px;
    }

  9. Lee James

    I already left a reply but for whatever reason it wasn’t published. This modification of your original snippet fixes the problem where additional space is added above the :target. Let me point out that the ‘extra’ space is noticeable closer to the bottom of a page where there is insufficient content to scroll to the desired :target. Although, if you scroll back a little after reaching any :target you may notice the extra space (particularly at paragraph separation areas).

    This modification effectively removes the appearance of this extra space by firstly adding the negative margin to :target {} and then the height to :target:before {} with the benefit of not interfering with vertical flow. The margin of :target {} negates the visual height asserted by :target:before {} while still reaching the intended ‘scrollto’ position (at 100px above the :target target).

    I hope that was clear enough. Here it is:

    :root {
    scroll-behavior: smooth;
    }

    :target {
    margin: -100px 0 0;
    }

    :target:before {
    content: “”;
    display: block;
    height: 100px;
    }

    1. Thanks for adding this Lee.

    2. Hi Lee,

      I tried your method on a local version of this site, and it didn’t quite work as expected. It looks fine on page load, but when you activate the anchor, it takes on the negative margin. Here’s s a screenshot: https://share.getcloudapp.com/qGuR5j4O

      I used your exact css:

      :root {
      scroll-behavior: smooth;
      }
      
      :target {
      margin: -100px 0 0;
      }
      
      :target:before {
      content: “”;
      display: block;
      height: 100px;
      }
      1. Lee James

        Without seeing the code in page context it’s difficult to diagnose.

        That said, the issue I drew attention to – that my modification corrects – is prevalent when :target is a container such as

        <section>, for example. It looks like I omitted that information from my original comment (oops). When applied directly to an inline heading :target, it’s not surprising that is should manifest much like the image you linked to demonstrates.

        The modification was quickly tested in Firefox (with default browser styles and an inherited display:block applied to the :target [section] element). It was untested in other browsers.

        The whole scenario is really an edge-case, as most times people will just want to set a heading as the link :target, and would arguably never see any undesirable artifacts. It’s also possible that the behavior differs dependent on base or reset stylesheet(s) applied to your testing environment.

        What I observed is probably hard to anticipate due to :target being an object with a pseudo-element [:target::before] that doesn’t actually exist prior to activating the link.

        Sometimes I think Schrödinger Cat is easier to wrap your head around, than CSS!

        1. Okay that makes sense. The solution I originally used is general and not specific to any one container. Cheers!

  10. Lee James

    Welcome. With the potential for page views, even older posts are deserving of occasional enhancement.

  11. Hi, thanks for sharing this but I can’t get it to work – I’m using a free WordPress theme (Hestia) and adding this to the Custom CSS box. The scroll is smooth but the section it goes to is still hidden behind the menu bar.
    Can anyone help please?
    Thanks

    1. Hi Rebecca,

      I’m not seeing the target:before style set in your stylesheet. Are you sure you’ve set it correctly?

  12. Matt

    I am wondering why you wouldn’t just use scroll-margin-top: 100px; on the element you are scrolling to here. No need to pseudo elements or anything. I guess you can’t do a global style but if you assigned to a class you could.

    1. Hey Matt, I hadn’t heard of that property before, I’ll certainly give it a test. Thanks for bringing it up. Guess I better check my @css-tricks feed to since I seem to be falling behind ¯\_(ツ)_/¯

    2. Dustin

      scroll-margin-top worked great for me and I do prefer that over using pseudo elements. I was able to assign it to all ‘a’ tags without a problem.

      Also just learned of the scroll-behavior property thanks to this page. Now I can get rid of that block of javascript I was using.

    3. Lee James

      Hi Matt. When you consider that the article was likely written back in 2020, when the CSS Scroll Snap Module was in its infancy, its easy to understand why scroll-top-margin wasn’t used for the original demo. Now, two years on, it’s certainly more usable.

      As a global style, you could do something like this:

      :root {
      scroll-behavior: smooth;
      }

      h1:target, h2:target, h3:target, h4:target, h5:target, h6:target, p:target {
      scroll-margin-top: 1rem;
      background: lightyellow;
      }

      Or, to be sassy (if pre-processing is your thing):

      h1, h2, h3, h4, h5, h6, p {
      &:target {
      scroll-margin-top: 1rem;
      }
      }

      No classes were harmed during this demo. Cheers.

      1. Lee James

        scroll-top-margin should be scroll-margin-top (excuse the typo).

Leave a Reply

Your email address will not be published. Required fields are marked *