Smooth Scroll Offset Anchor Links with CSS

Don’t you hate it when you create an anchor link and it gets covered up by a fixed header? And isn’t it a bummer when the browser jumps to that link instantaneously all abrupt-like?

Well here’s a quick CSS snippet to create a lightweight, CSS based way to offset anchor links, and have them scroll smoothly:

html {
    scroll-behavior: smooth;
}

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

CSS Smooth Scroll

The scroll-behavior is a CSS property that’s got some – but not full – browser support so you mileage will vary. My browser of choice, Safari, does not currently support this property, but I’ve implemented it on this site anyway. You can add it to an individual element, or enable site wide on html.

Offset Anchors

There are quite a few offset anchor tutorials out there, but most target the :target (heh heh heh) pseudo-class directly, hiding and moving it around. The only way this really works is to set the anchor on an additional element so you don’t lose the actual anchor you want to skip to. Something like this:

// Do not use this
:target {
  display: block;
  position: relative;
  top: -100px; 
  visibility: hidden;
}

The key here is the additional :before pseudo element. This allows you to keep the markup clean while still offsetting, and not have the target disappear.

Offset Smooth Anchor Demo

Click this link right here and you should be smoothly whisked to a section above. Unless you’re on Safari. So abrupt!

Bonus points to those who try it and can leave a comment about which section it links to.

29 thoughts on “Smooth Scroll Offset Anchor Links with CSS

  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.

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

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

  5. 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;
    }

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

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

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

Leave a Reply

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

Ready for a refreshing experience on your next website design?