Back to Top Button ปุ่มกลับขึ้นด้านบน
A floating button that appears after you scroll down, shows how far you are, and scrolls smoothly back up.
Live demo
Everything here talks to the real Spring Boot + SQL Server back end.This lesson is a long page on purpose. Scroll down: after 400 pixels a round button appears at the bottom right. Its ring fills as you go further, and clicking it brings you back to the top.
Why a back-to-top button?
On long pages such as articles, product lists or search results, people reach the end and want to go back to the menu or the filters. On a phone there is no Home key, and dragging all the way up is tiring. A small button gives a one-tap way back.
When to show it
Only after the person has scrolled some distance. A button that is always visible covers content and adds noise. Here the threshold is 400 pixels, about half a phone screen.
Smooth, but not for everyone
The page scrolls smoothly with scrollTo({ behavior: 'smooth' }). People who asked their system to reduce motion get an instant jump instead.
Keyboard and screen readers
After the scroll, focus moves to the main content, so a keyboard user continues from the top instead of from the hidden button. The button has an aria-label because it shows only an arrow.
Performance
Scroll events fire many times per second. The handler is passive and only schedules one update per animation frame with
requestAnimationFrame, so scrolling stays smooth even on slow devices.
The progress ring
The ring is an SVG circle. Its dash length equals the circumference (2πr). Moving stroke-dashoffset from the full length to zero draws the ring from empty to full.
Placement
Bottom right is where people expect it. When a toast appears in the same corner, the button hides for a moment so they do not overlap.
Try it
Keep scrolling to the end of this page, then press the button, or press Home to compare with the built-in key.
Extra section 1
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
Extra section 2
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
Extra section 3
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
Extra section 4
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
Extra section 5
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
Extra section 6
Spacing, line length and contrast make long text easier to read. Lines of about 60 to 75 characters, a line height of 1.6 and clear headings let people scan the page and find their place again after scrolling back up.
You reached the end. Now use the button at the bottom right.
What to notice
- A passive scroll listener with requestAnimationFrame keeps scrolling smooth.
- The ring is an SVG circle with stroke-dashoffset = progress.
- Focus moves to the top of the page for keyboard users.
Try it
Source code
4 parts, read from the files in this project<button class="back-to-top" type="button" id="backToTop" aria-label="Back to top" data-tip="Back to top" data-tip-pos="left">
<svg class="ring" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="21" class="ring-bg"/><circle cx="24" cy="24" r="21" class="ring-fg"/></svg>
<svg class="i"><use href="#i-arrow-up"/></svg>
</button>
.back-to-top {
position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 90; width: 3rem; height: 3rem; border-radius: 50%; border: 0; cursor: pointer;
display: grid; place-items: center; background: var(--surface); color: var(--primary-text); box-shadow: var(--shadow-lg);
opacity: 0; transform: translateY(1rem) scale(.9); pointer-events: none; transition: opacity .25s, transform .25s var(--ease);
}
.back-to-top.show { opacity: 1; transform: none; pointer-events: auto; }
.back-to-top:hover { background: var(--primary); color: var(--on-primary); }
.back-to-top .ring { position: absolute; inset: 0; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 3; }
.ring-bg { stroke: var(--surface-3); }
.ring-fg { stroke: var(--primary); stroke-linecap: round; stroke-dasharray: 132; stroke-dashoffset: 132; }
.back-to-top .i { position: relative; width: 1.25rem; height: 1.25rem; }
body:has(.toast-stack[data-pos="bottom-right"] .toast) .back-to-top { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
.spinner, .btn.is-loading::after { animation: spin 1.5s linear infinite !important; animation-duration: 1.5s !important; animation-iteration-count: infinite !important; }
}
(() => {
const btn = $('#backToTop');
const ring = $('.ring-fg', btn);
const length = 2 * Math.PI * 21;
ring.style.strokeDasharray = length;
let ticking = false;
function update() {
ticking = false;
const max = document.documentElement.scrollHeight - innerHeight;
const ratio = max > 0 ? scrollY / max : 0;
ring.style.strokeDashoffset = length * (1 - ratio);
btn.classList.toggle('show', scrollY > 400);
document.dispatchEvent(new CustomEvent('scroll:progress', { detail: ratio }));
}
addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true });
btn.addEventListener('click', () => {
scrollTo({ top: 0, behavior: reducedMotion() ? 'auto' : 'smooth' });
$('#main').focus({ preventScroll: true }); // keyboard users continue from the top
});
update();
})();