Back to Top

Scroll down this page: the button appears in the bottom corner.

@use 'sprucecss/scss/spruce' as *;

.back-to-top {
  box-shadow: shadow('lg');
  inset-block-end: spacer('m');
  inset-inline-end: spacer('m');
  opacity: 0;
  position: fixed;
  translate: 0 1rem;
  visibility: hidden;
  z-index: z-index('fixed');
  @include transition($property: 'opacity, translate, visibility');

  &.is-visible {
    opacity: 1;
    translate: 0 0;
    visibility: visible;
  }
}
<p class="back-to-top-demo">Scroll down this page: the button appears in the bottom corner.</p>
<a class="btn btn--primary btn--icon back-to-top" href="#top" data-back-to-top aria-label="Back to top">
    <svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m18 15-6-6-6 6"/></svg>
</a>
(() => {
  const button = document.querySelector('[data-back-to-top]');

  if (!button) {
    return;
  }

  const sentinel = document.createElement('div');
  sentinel.style.cssText = 'position: absolute; inset-block-start: 0; block-size: 100vh; inline-size: 1px; pointer-events: none;';
  document.body.prepend(sentinel);

  new IntersectionObserver(([entry]) => {
    button.classList.toggle('is-visible', !entry.isIntersecting);
  }).observe(sentinel);

  button.addEventListener('click', (event) => {
    event.preventDefault();

    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });

    const target = document.querySelector('h1, main') ?? document.body;
    target.setAttribute('tabindex', '-1');
    target.focus({ preventScroll: true });
  });
})();

Back to Top

On this page

A floating "Back to top" link that appears after the user scrolls down.

Technical Details

  • It is a link to #top (it works without JavaScript); the script moves the focus to the top of the page too, so keyboard users continue from there.
  • It is hidden with visibility (not just opacity) when not needed, so it isn't focusable while invisible.
  • The scroll respects prefers-reduced-motion: smooth only when motion is OK.
  • The visibility is toggled by an IntersectionObserver on a sentinel element, which is cheaper than a scroll listener.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.