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.
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.
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.
commenting to follow
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.
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.
Awesome! Thanks for this Bryan 🙂
No problem 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;
}
Do you have a URL I could take a look at?
Sure. https://boydells.oxwebdevelopment.com.au/ Click on the white arrow in the first section to scroll down 🙂
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
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.
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
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!
This is great!
🙂
But it does not work with a negative text-indent p tag.
🙁
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;
}
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;
}
Thanks for adding this Lee.
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:
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!
Okay that makes sense. The solution I originally used is general and not specific to any one container. Cheers!
Welcome. With the potential for page views, even older posts are deserving of occasional enhancement.
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
Hi Rebecca,
I’m not seeing the
target:before
style set in your stylesheet. Are you sure you’ve set it correctly?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.
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 ¯\_(ツ)_/¯
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.
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.
should bescroll-top-margin
scroll-margin-top
(excuse the typo).