Designing a “Back to Top” Button

A “Back to Top” button is small, but it changes how people experience long pages. In this build, you will design a back-to-top control that looks polished, feels fast, and respects accessibility. You will create a clean circular button, draw the up icon with pure CSS (no images), reveal it only after scrolling, and add smooth motion that disables itself when a user prefers less animation.

Why Back to Top Buttons Matter

Readers reach the end of an article and need a quick escape back to navigation, search, or a sticky header. A keyboard-friendly, touch-sized button gives them that exit without hunting for scrollbars or trackpad gestures. Building it yourself means you keep design control, avoid image assets, and theme it with CSS variables. The result loads instantly, renders crisply on any screen, and fits your brand without shipping a whole icon set.

Prerequisites

You do not need a framework. We will work with a single HTML page, modern CSS, and a tiny bit of JavaScript for show and hide behavior. If JavaScript does not run, the link still jumps to the top.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The markup is intentionally minimal. We place an anchor at the top for the target, content that creates scroll, and a fixed-position control near the bottom-right. The control is an anchor for no‑JS fallback, with visually hidden text for screen readers. A deferred script file will toggle visibility as the user scrolls.

<!-- HTML -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Back to Top Demo</title>
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <style>/* CSS is added in later steps */</style>
</head>
<body>
  <header id="top">
    <h1>Article Title</h1>
  </header>

  <main>
    <article class="content">
      <h2>Section One</h2>
      <p>Long form content goes here. Repeat sections to create scroll.</p>
      <h2>Section Two</h2>
      <p>More content...</p>
      <h2>Section Three</h2>
      <p>More content...</p>
      <h2>Section Four</h2>
      <p>More content...</p>
      <h2>Section Five</h2>
      <p>More content...</p>
    </article>
  </main>

  <a href="#top" class="back-to-top" aria-label="Back to top">
    <span class="visually-hidden">Back to top</span>
  </a>

  <script src="back-to-top.js" defer></script>
</body>
</html>

Step 2: The Basic CSS & Styling

Set up color tokens and sizes with CSS variables so theming is trivial. Use smooth scrolling for anchor jumps, apply a sensible reading width, and create a circular button with a fixed position at the bottom-right. The button starts hidden; a later step will toggle a class to reveal it. The icon will be painted with a pseudo-element, so the anchor itself holds only accessible text.

/* CSS */

/* CSS */
:root {
  --bt-size: 48px;
  --bt-bg: hsl(220 15% 15% / 0.85);
  --bt-fg: white;
  --bt-border: hsl(0 0% 100% / 0.2);
  --bt-shadow: 0 10px 25px hsl(220 30% 10% / 0.35);
  --bt-offset: 1.25rem;
  --bt-z: 9999;
  --bt-focus: 2px solid hsl(210 90% 60%);
}

html {
  scroll-behavior: smooth;
}

body {
  font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  margin: 0;
  color: hsl(220 15% 15%);
  background: hsl(0 0% 100%);
}

header {
  padding: 2rem 1rem 1rem;
  background: hsl(220 20% 97%);
  border-bottom: 1px solid hsl(220 20% 90%);
}

.content {
  max-width: 70ch;
  margin: 2rem auto 6rem;
  padding: 0 1rem;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.back-to-top {
  position: fixed;
  right: var(--bt-offset);
  bottom: var(--bt-offset);
  width: var(--bt-size);
  height: var(--bt-size);
  border-radius: 50%;
  background: var(--bt-bg);
  color: var(--bt-fg);
  border: 1px solid var(--bt-border);
  box-shadow: var(--bt-shadow);
  display: grid;
  place-items: center;
  text-decoration: none;
  z-index: var(--bt-z);

  /* Hidden by default; Step 4 toggles .is-visible */
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px) scale(0.98);
  transition:
    opacity 180ms ease,
    transform 180ms ease,
    visibility 0s linear 180ms;
  pointer-events: none;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1);
  transition:
    opacity 200ms ease,
    transform 200ms ease,
    visibility 0s;
  pointer-events: auto;
}

.back-to-top:focus-visible {
  outline: var(--bt-focus);
  outline-offset: 3px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bt-bg: hsl(220 15% 10% / 0.85);
    --bt-border: hsl(0 0% 100% / 0.18);
    --bt-shadow: 0 10px 25px hsl(220 40% 5% / 0.6);
  }
}

Advanced Tip: The circle is a direct use of border-radius: 50%. If you want a refresher on shape math, this pattern is covered in detail in how to make a circle with CSS. Using variables for color and size gives you a theme switch with a single change at :root.

Step 3: Building the Button Icon and Layout

The icon sits in a pseudo-element so the anchor stays clean and semantic. A chevron reads well at small sizes and keeps stroke contrast high. We will draw it with borders and a rotation, keeping it vector-sharp on any display.

/* CSS */

/* CSS */
.back-to-top::before {
  content: "";
  width: 0.7em;
  height: 0.7em;
  border-style: solid;
  border-color: currentColor;
  border-width: 2px 0 0 2px; /* top and left sides form the chevron */
  transform: rotate(45deg) translateY(1px);
  transform-origin: 50% 50%;
}

.back-to-top:hover {
  filter: brightness(1.05);
}

.back-to-top:active {
  transform: translateY(1px) scale(0.98);
}

How This Works (Code Breakdown)

The icon uses a single pseudo-element. Setting content: “” creates a drawable box without extra markup. The element is sized in em so it scales with the button’s font-size if you adjust it in the future. The border trick gives you two strokes at right angles; rotating by 45 degrees turns those strokes into a chevron that points up.

The chevron’s color comes from currentColor, which inherits from the link. This lets themes change one color token and recolor both background and icon in sync. The hover rule nudges brightness for feedback. On :active, a subtle press effect shifts and scales the button by a single pixel feel. If you prefer a filled triangle instead of a chevron, the classic border triangle pattern works well and is described here: make a triangle up with CSS. If your visual language favors chevrons, there is also a step-by-step for the exact icon we drew here: CSS chevron up.

Step 4: Behavior and Visibility with JavaScript

The button should appear only after the reader moves down the page. Intersection Observer is a great fit for this: observe a top sentinel and toggle a class when that sentinel leaves the viewport. Add a lightweight scroll fallback to cover older browsers. The anchor href still works if JavaScript never runs.

/* CSS */

/* CSS */
.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1);
  transition:
    opacity 200ms ease,
    transform 200ms ease,
    visibility 0s;
  pointer-events: auto;
}

/* JS */

/* JS */
(() => {
  const btn = document.querySelector('.back-to-top');
  const topTarget = document.querySelector('#top');

  if (!btn || !topTarget) return;

  // Reveal when #top is out of view
  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries) => {
      const entry = entries[0];
      if (entry.isIntersecting) {
        btn.classList.remove('is-visible');
      } else {
        btn.classList.add('is-visible');
      }
    });
    io.observe(topTarget);
  } else {
    // Fallback: threshold in pixels
    const toggle = () => {
      if (window.scrollY > 400) {
        btn.classList.add('is-visible');
      } else {
        btn.classList.remove('is-visible');
      }
    };
    toggle();
    window.addEventListener('scroll', toggle, { passive: true });
  }

  // Improve keyboard behavior on Enter/Space if a button role is used in variants
  btn.addEventListener('keyup', (e) => {
    if (e.key === ' ' || e.key === 'Enter') {
      btn.click();
    }
  });
})();

How This Works (Code Breakdown)

Observing the #top element lets the browser tell you when the page’s start is visible. The callback flips a class on the back-to-top anchor. This approach is fast and keeps work off the main thread compared to a high-frequency scroll listener. The fallback covers browsers that do not support Intersection Observer by measuring scrollY and toggling the class after a threshold. Because the control is an anchor with href=”#top”, the jump to the top still works with no script. Smooth scrolling comes from the html rule in Step 2.

Advanced Techniques: Animations, Themes, and Hover Effects

Subtle motion helps users notice the control without distracting from the content. Keep the animation short, low amplitude, and easy to disable. Use variables for colors and a shadow ring hover to match brand tokens or switch themes with a single override.

/* CSS */

/* CSS */
@keyframes bt-pop {
  from {
    transform: translateY(6px) scale(0.96);
    opacity: 0;
  }
  to {
    transform: translateY(0) scale(1);
    opacity: 1;
  }
}

.back-to-top.is-visible {
  animation: bt-pop 160ms ease-out;
}

.back-to-top:hover::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  box-shadow: 0 0 0 6px color-mix(in oklab, var(--bt-bg), white 30%);
  opacity: 0.15;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .back-to-top,
  .back-to-top.is-visible {
    transition: none;
    animation: none;
    transform: none;
  }
}

This animation only plays when the control first appears, then normal hover and active states handle feedback. The @media query disables motion for people who prefer calmer interfaces. If your style requires a different pictogram, swapping the pseudo-element to a filled triangle takes one rule edit and can follow the pattern in the triangle guide linked earlier. You can also adjust the circle size by changing –bt-size; the chevron scales with em, keeping the ratio tight at any size.

Design Note: If your design language leans toward geometric strokes and arrows, a chevron reads crisper at small sizes. If your brand favors solid shapes, a filled triangle may look stronger. Both are doable with borders and transforms, no images needed.

Accessibility & Performance

Accessibility

The control uses an accessible name via aria-label and includes visually hidden text. The focus outline is visible on any background and has enough contrast to meet guidelines. Target size is at least 44px, which helps on touch screens and improves click comfort on desktops. Because the control is an anchor, it participates in the tab order and supports Enter by default. If you swap to a button element, keep the same aria-label and focus styles. The prefers-reduced-motion query cuts animations to respect user settings.

Placement matters. Keep the button a safe distance from chat widgets or cookie banners. Use z-index only as high as needed and test across breakpoints to prevent overlap with content or navigation.

Performance

A single fixed element with a borderline icon is trivial for the rendering engine. Transforms and opacity animate on the compositor, which stays smooth on modern devices. Intersection Observer is event-driven and runs less work than scroll polling. Avoid heavy box-shadow animations on every frame; switch states, then let the GPU handle the rest. Using pure CSS for the icon means no network fetch for SVG or PNG, and no layout shifts from late-loading assets.

Small UI, Strong Payoff

You built a back-to-top control that is crisp, keyboard friendly, and easy to theme. You drew the icon with CSS, revealed it at the right time, and added motion that adapts to user settings. Now you can drop this pattern into any long page, or expand it into a floating UI kit. For variations, try a solid arrow following the triangle pattern or a thin-stroke version from the chevron guide, then tune the shape to your brand.

Leave a Comment