This is the fourth post in a series examining modern CSS solutions to problems I've been solving over the last 13+ years of being a frontend developer. Visit ModernCSS.dev to view the whole series and additional resources.
"Back to top" links may not be in use often these days, but there are two modern CSS features that the technique demonstrates well:
position: sticky
scroll-behavior: smooth
I first fell in love with "back to top" links - and then learned how to do them with jQuery - on one of the most gorgeous sites of 2011: Web Designer Wall.
The idea is to provide the user with a "jump link" to scroll back to the top of the website and was often used on blogs of yore.
Here's what we will learn to achieve:
About position: sticky
This newer position value is described as follows on caniuse:
Keeps elements positioned as "fixed" or "relative" depending on how it appears in the viewport. As a result, the element is "stuck" when necessary while scrolling.
The other important note from caniuse data is that you will need to offer it prefixed for the best support. Our demo will fallback to position: fixed
which will achieve the main goal just a bit less gracefully.
About scroll-behavior: smooth
This is a very new property, and support is relatively low. This exact definition requests that scrolling behavior, particularly upon selection of an anchor link, has a smoothly animated appearance versus the default, more jarring instant jump.
Using it offers "progressive enhancement" meaning that it will be a better experience for those whose browsers support it, but will also work for browsers that don't.
Surprisingly, Safari is behind on supporting this, but the other major browsers do.
Set the Stage: Basic content HTML
First, we need to setup some semantic markup for a basic content format:
<header id="top">Title</header>
<main>
<article>
<!-- long form content here -->
</article>
<!-- Back to Top link -->
<div class="back-to-top-wrapper">
<a href="#top" class="back-to-top-link" aria-label="Scroll to Top">๐</a>
</div>
</main>
We place our link after the article
, but within main
. It isn't specifically part of the article, and we also want it to be last in focus order.
We also add id="top"
to the <header>
and use that anchor as the href
value for the back to top link. If you only wanted to scroll to the top of <main>
you can move the id, or also attach it to an existing id near the top of your page.
Add smooth-scrolling
The first part of our objective is easy peasy and is accomplished by the following CSS rule:
/* Smooth scrolling IF user doesn't have a preference due to motion sensitivities */
@media screen and (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
h/t to @mhlut for pointing out that the original solution was not accounting for prefers-reduced-motion
Previously, I had this CSS-Tricks article bookmarked to accomplish this with jQuery and vanilla JS. The article has been around a while, and kudos to that team for continually updating articles like that when new methods are available ๐
I have found some oddities, such as when you visit a page that includes an #anchor
in the URL it still performs the smooth scroll which may not actually be desirable for your scenario.
Style the "Back to Top" link
Before we make it work, let's apply some basic styling to the link. For fun, I used an emoji but you can swap for an SVG icon for more control over styling.
.back-to-top-link {
display: inline-block;
text-decoration: none;
font-size: 2rem;
line-height: 3rem;
text-align: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: #D6E3F0;
/* emoji don't behave like regular fonts
so this helped position it correctly */
padding: 0.25rem;
}
This gives us a very basic round button. In the full Codepen, I've added additional "pretty" styles and :hover
and :focus
styling, but those aren't essential.
Next, you may wonder why we added a wrapper for this link. The reason is we need to use it to basically create a "scroll track" for the link to live within.
The way position: sticky
is designed, it picks up the element from where it's positioned in the DOM. Then, it respects a "top" value to place it relative to the viewport. However, we placed the link at the end of the document, so it would essentially never be picked up without some assistance.
We will use the wrapper along with position: absolute
to alter the link's position to visually higher up on the page. Thankfully, the browser uses this visually adjusted position - aka when it enters the viewport - to calculate when to "stick" the link.
So, our wrapper styles look like this:
/* How far of a scroll travel within <main> prior to the
link appearing */
$scrollLength: 100vh;
.back-to-top-wrapper {
// uncomment to visualize "track"
// outline: 1px solid red;
position: absolute;
top: $scrollLength;
right: 0.25rem;
// Optional, extends the final link into the
// footer at the bottom of the page
// Set to `0` to stop at the end of `main`
bottom: -5em;
// Required for best support in browsers not supporting `sticky`
width: 3em;
// Disable interaction with this element
pointer-events: none;
}
The last definition may also be new to you: pointer-events: none
. This essentially lets interaction events like clicks "fall through" this element so that it doesn't block the rest of the document. We wouldn't want this wrapper to accidentally block links in the content, for example.
With this in place, we now see the link overlapping the content a little bit below the initial viewport content. Let's add some styling to <main>
to prevent this overlap, and also add position: relative
which is necessary for best results given the absolute link wrapper:
main {
// leave room for the "scroll track"
padding: 0 3rem;
// required to make sure the `absolute` positioning of
// the anchor wrapper is indeed `relative` to this element vs. the body
position: relative;
max-width: 50rem;
margin: 2rem auto;
// Optional, clears margin if last element is a block item
*:last-child {
margin-bottom: 0;
}
}
All that's left is to revisit the link to add the necessary styles for the positioning to full work:
.back-to-top-link {
// `fixed` is fallback when `sticky` not supported
position: fixed;
// preferred positioning, requires prefixing for most support, and not supported on Safari
// @link https://caniuse.com/#search=position%3A%20sticky
position: sticky;
// reinstate clicks
pointer-events: all;
// achieves desired positioning within the viewport
// relative to the top of the viewport once `sticky` takes over, or always if `fixed` fallback is used
top: calc(100vh - 5rem);
// ... styles written earlier
}
The fixed
fallback means that browsers that don't support sticky
positioning will always show the link. When sticky
is supported, the fully desirable effect happens which is the link will not appear until the $scrollLength
is passed. With sticky
position, once the top of the wrapper is in the viewport, the link is "stuck" and scrolls with the user.
Notice we also reinstated pointer-events
with the all
value so that interaction with the link actually work.
And here's the final result - view in non-Safari for best results.
Known Issues
If you have short-form content, this doesn't fail very gracefully on a shorter device viewport. You may think - as did I - to use overflow: hidden
. But that, unfortunately, prevents position: sticky
from working entirely โน๏ธ So in a "real world" scenario, you may have to offer an option to toggle this behavior per article, or perform a calculation to determine if an article meets a length requirement before injecting it in the template.
Drop a comment if you know of or find any other "gotchas" with these methods!
Top comments (8)
Thank you! I love scroll-to-top links and have been wanting a more graceful solution for this. This looks very good.
I also love smooth scroll, I only recently read that people who are sensitive to movement might have
prefers-reduced-motion
flag on, and you would disable smooth scrolling for them. I haven't read into it further yet, but so far it seems to make sense.Really good point on motion sensitivities, I'll update to include that! Glad you found this useful ๐
Just discovered your excellent Modern CSS Solutions site via Kevin Powell and I'm going through your articles and I'm testing some of your solutions to possibly implement in my soon-to-be redone website... I'm testing in Codepen what you have done here re: the smooth scrolling and it is NOT working in Codepen using Brave or Firefox. I haven't tested stand-alone, but do you know if there are issues with Codepen not being up to date with things like smooth-scrolling? I thought it might have been the prefers-reduced-motion flag that wasn't recognized, but I put the smooth scroll command outside the media query with no difference. No difference also when I change the layout of the windows in Codepen. I'm using Win10.
Do you mean your own re-implementation isn't working, or my original demo isn't working? I'm not aware of an issue (it is still working for me) and caniuse shows support for Firefox but I'm not familiar with Brave: caniuse.com/css-scroll-behavior
Thank you for replying Stephanie!
It's in the Codepen original demo. I've tried in Firefox, Chrome and Brave (faster chromium based browser) and I don't see a difference (even when commenting out the property). Ditto on the MDN site. It's not clear to me what this smooth scrolling action should look like, all I see is BOOM, right back to the top. User agents can ignore the property, but I checked the settings of each browser and smooth scrolling is enabled (in my Win10 settings as well) ... unless it's an add-on/extension that is the culprit (I haven't gone to that extent in my testing though).
I know it's not a big deal, but it does raise a couple of questions: a) how can I properly test something if I can't even see it locally despite the property being implemented in most browsers? and b) can I trust tools such as Codepen and the like to give me an unbiased result? I mean...smooth scrolling isn't going to break a site, but...what else WILL, if I am given the impression things are hunky dory (or not) and I implement a site following what I'm being "told" is functional? You know what I mean? I'm a toddler with web dev so maybe I'm barking up the wrong tree, but I was a programmer in a former life. Oh and I also noticed the Top button is displaying sort of, well, weird (I include a screenshot to show you). Best regards!
I've just been able to test on Win10 and it's working (testing my CodePen demo, no changes) so I'm sorry but I'm not sure what to suggest!
I probably shouldn't have used an emoji - the Mac version of the "top" emoji is more minimal but I see the Win is blocky :) I will add a note to update it to perhaps an SVG for more consistent results, thanks for the callout!
CodePen shouldn't be explicitly getting in the way of rendering. The only time I've occasionally run into issues is if I keep the "Normalize" CSS reset which is a bit heavy handed for modern browsers. I just removed it from the Pen juuuuust in case it makes a difference! If not, I'm sorry but I'm not sure what else other than something unique to your system. Good luck with your web dev journey!
Ok, thanks!
I retested on the MDN site after pressing Submit and it WAS working (I must have not tested correctly), but still NOT working on the Codepen (which I reloaded).
Oh, wait....(breaking news sound effect)...I just found (via MDN's super documentation) a setting in my Win10 setup re: showing animations...and it was turned OFF! Activating it made the smooth scroll work on your Codepen! Hhaaa!
Well...isn't that interesting and educational! LOOL! (mumbles something inaudible about Windows...)
Sorry to have bothered you with this...I had a feeling it was something higher up in the programming food chain!
Now I can continue going through your interesting articles on your site! :-) ^_^
Glad you got it worked out!