UI Components Easy
Back to Top
Floating back-to-top button that appears after scrolling down, with smooth scroll animation and fade in/out.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--text: #f2f6ff;
--muted: #475569;
--border: rgba(255, 255, 255, 0.08);
--accent: #38bdf8;
--btn-size: 52px;
--btn-offset: 1.75rem;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: Inter, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* โโ Demo โโ */
.demo {
max-width: 640px;
margin: 0 auto;
padding: 3rem 1.5rem 6rem;
}
.page-header {
margin-bottom: 3rem;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: var(--muted);
font-size: 0.875rem;
}
/* โโ Articles โโ */
.articles {
display: flex;
flex-direction: column;
gap: 2rem;
}
.article {
padding: 1.5rem;
border-radius: 14px;
background: var(--card);
border: 1px solid var(--border);
}
.article__title {
font-size: 1.0625rem;
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.01em;
}
.article__body {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.75;
}
.article__body code {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.8125em;
background: rgba(255, 255, 255, 0.07);
padding: 0.1em 0.4em;
border-radius: 4px;
color: var(--accent);
}
/* โโ Back-to-top button โโ */
.btt-btn {
position: fixed;
bottom: var(--btn-offset);
right: var(--btn-offset);
width: var(--btn-size);
height: var(--btn-size);
display: flex;
align-items: center;
justify-content: center;
background: rgba(13, 17, 23, 0.9);
border: 1px solid var(--border);
border-radius: 50%;
cursor: pointer;
color: var(--text);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
/* Hidden by default */
opacity: 0;
pointer-events: none;
transform: translateY(12px) scale(0.9);
transition: opacity 0.3s var(--ease), transform 0.3s var(--ease), box-shadow 0.2s;
z-index: 999;
}
.btt-btn.visible {
opacity: 1;
pointer-events: all;
transform: translateY(0) scale(1);
}
.btt-btn:hover {
box-shadow: 0 6px 32px rgba(56, 189, 248, 0.25);
transform: translateY(-2px) scale(1.05);
}
.btt-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.btt-btn:active {
transform: scale(0.96);
}
/* โโ Progress ring โโ */
.btt-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
border-radius: 50%;
overflow: visible;
}
.btt-ring__track {
fill: none;
stroke: rgba(255, 255, 255, 0.06);
stroke-width: 2.5;
}
.btt-ring__fill {
fill: none;
stroke: var(--accent);
stroke-width: 2.5;
stroke-linecap: round;
/* set dynamically by JS: stroke-dasharray and stroke-dashoffset */
transition: stroke-dashoffset 0.1s linear;
}
/* โโ Arrow icon โโ */
.btt-arrow {
position: relative;
z-index: 1;
color: var(--text);
transition: transform 0.2s var(--ease);
}
.btt-btn:hover .btt-arrow {
transform: translateY(-1px);
}(function () {
var THRESHOLD = 300; // px before button appears
var btn = document.getElementById("btt-btn");
var ringFill = document.getElementById("btt-ring-fill");
if (!btn) return;
// โโ Progress ring setup โโ
var radius = 23; // matches SVG r attribute
var circumference = 2 * Math.PI * radius; // ~144.51
if (ringFill) {
ringFill.style.strokeDasharray = circumference;
ringFill.style.strokeDashoffset = circumference; // fully hidden
}
// โโ Scroll handler โโ
function onScroll() {
var scrollY = window.scrollY || window.pageYOffset;
var maxScroll = document.documentElement.scrollHeight - window.innerHeight;
var pct = maxScroll > 0 ? scrollY / maxScroll : 0;
// Show / hide button
if (scrollY > THRESHOLD) {
btn.classList.add("visible");
} else {
btn.classList.remove("visible");
}
// Update progress ring
if (ringFill) {
var offset = circumference * (1 - pct);
ringFill.style.strokeDashoffset = offset;
}
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll(); // initialise on load
// โโ Click: smooth scroll to top โโ
btn.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
// โโ Keyboard activation โโ
btn.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Back to Top</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- โโ Page content (scrollable) โโ -->
<div class="demo">
<header class="page-header">
<h1 class="demo-title">Back to Top</h1>
<p class="demo-sub">Scroll down to see the floating button appear. Click it to return here.</p>
</header>
<div class="articles">
<article class="article">
<h2 class="article__title">The Art of Scroll UX</h2>
<p class="article__body">Long-form content โ documentation, blog posts, landing pages โ benefits enormously from a well-placed back-to-top button. Rather than forcing users to scroll all the way back, a single click returns them instantly. The key is subtlety: the button should only appear when it is needed and should never obscure content while reading.</p>
</article>
<article class="article">
<h2 class="article__title">Why Position: Fixed</h2>
<p class="article__body">The button uses <code>position: fixed</code> anchored to the bottom-right corner of the viewport. This keeps it consistently reachable regardless of scroll position, layout width, or device. A small <code>margin</code> from the edges prevents it from hugging too close to the browser chrome on mobile.</p>
</article>
<article class="article">
<h2 class="article__title">Smooth Scroll Performance</h2>
<p class="article__body">Using <code>window.scrollTo({ top: 0, behavior: 'smooth' })</code> delegates animation to the browser's native compositor, which can run off the main thread. This avoids the jank of JS-driven scroll loops while still providing a polished, animated experience across all major browsers.</p>
</article>
<article class="article">
<h2 class="article__title">The Progress Ring</h2>
<p class="article__body">This variant adds a circular SVG progress ring around the button that fills as you scroll down the page. The ring is drawn with a <code>stroke-dashoffset</code> updated on every scroll frame. The circumference of the circle is pre-calculated and stored as a CSS custom property so the math stays in JavaScript but the animation stays in CSS.</p>
</article>
<article class="article">
<h2 class="article__title">Visibility Threshold</h2>
<p class="article__body">The button appears after 300 px of scroll โ roughly one viewport height on a phone. This threshold prevents the button from flashing in for short pages. You can adjust it by changing the <code>THRESHOLD</code> constant in the script. For very long pages, a higher threshold (500โ800 px) may feel more appropriate.</p>
</article>
<article class="article">
<h2 class="article__title">Accessibility Considerations</h2>
<p class="article__body">The button is always in the DOM so its focus state is predictable. It uses <code>opacity: 0</code> and <code>pointer-events: none</code> when hidden โ not <code>display: none</code> โ which means keyboard focus can still land on it. An <code>aria-label</code> provides a meaningful name for screen readers, and the <code>tabindex</code> is left at the default <code>0</code>.</p>
</article>
<article class="article">
<h2 class="article__title">Customization Tips</h2>
<p class="article__body">All visual tokens live in CSS custom properties: button size, accent color, offset from the edges, and transition timing. Override them at the <code>:root</code> level or scope them to a parent element. The progress ring radius and stroke width are derived from <code>--btn-size</code> automatically so the ring always fits the button perfectly.</p>
</article>
</div>
</div>
<!-- โโ Floating back-to-top button โโ -->
<button
class="btt-btn"
id="btt-btn"
aria-label="Back to top"
title="Back to top"
>
<!-- Progress ring -->
<svg class="btt-ring" aria-hidden="true" viewBox="0 0 52 52">
<circle class="btt-ring__track" cx="26" cy="26" r="23" />
<circle class="btt-ring__fill" id="btt-ring-fill" cx="26" cy="26" r="23" />
</svg>
<!-- Arrow icon -->
<svg class="btt-arrow" aria-hidden="true" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M9 14V4M4 9l5-5 5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<script src="script.js"></script>
</body>
</html>Back to Top
A floating action button that fades in after the user scrolls 300 px down the page and smooth-scrolls back to the top on click. No dependencies.
Features
- Fades and slides in from the bottom-right after 300 px of scroll
- Fades out when the page is near the top
- Smooth scroll to top via
window.scrollTo({ behavior: 'smooth' }) - Scale animation on hover for tactile feedback
- Reading-progress ring that fills as the user scrolls down the page
- Fully keyboard-accessible โ focusable and activatable with
Enter/Space
How it works
- A passive
scrollevent listener compareswindow.scrollYagainst the threshold (300 px) - When the threshold is crossed the button receives an
.visibleclass โ a CSS transition handles the fade/slide animation - A
<circle>SVG stroke-dashoffset is updated on scroll to draw the circular progress ring - Clicking the button calls
window.scrollTo({ top: 0, behavior: 'smooth' })
Accessibility
The button uses aria-label="Back to top" and is always in the tab order when visible. It is hidden via pointer-events: none and opacity: 0 (not display: none) so focus management is smooth.