jQuery / Create a Smooth Back-to-Top Link
Create a real href="#top" link, then enhance its click with jQuery .animate({ scrollTop: 0 }) on the document’s scrolling element. Prevent the immediate native jump while keeping the anchor for use without JavaScript. Respect reduced motion and move keyboard focus to the top heading after completion.
Choose the document scroller and fallback link
This example scrolls the whole page. document.scrollingElement returns its actual scrolling element, the document root in standards mode. Keep <!doctype html>. Selecting one actual scroller avoids separate effects and completion callbacks on both html and body.
Give the top heading id="top" and point the link to it. tabindex="-1" permits programmatic focus without adding the heading to normal Tab navigation. Scrolling alone may leave keyboard focus on the link below, so treat view movement and focus separately.
Complete single-file example
Save everything as UTF-8 in back-top-demo.html and open it in a browser. Internet loads the official full jQuery file 4.0.0. Timed animation requires effects, so do not substitute slim. No server or installation is needed.
Go to 900px establishes a comparison position; the lower-right link stays available. A short page cannot scroll, so the exercise has sufficient height. The fixed toolbar is for the demo; real placement should avoid existing headers and advertisements.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Smooth back-to-top link</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
html { scroll-behavior: auto; }
body { font: 16px/1.6 sans-serif; margin: 0; padding: 100px 20px; min-height: 2400px; box-sizing: border-box; }
#top { max-width: 760px; margin: 0; }
#top:focus { outline: 3px solid teal; }
#toolbar { position: fixed; top: 0; left: 0; right: 0; background: #fff; padding: 12px; z-index: 2; }
button, a { font: inherit; }
button { margin: 4px; }
#back { position: fixed; right: 16px; bottom: 16px; padding: 10px 14px; background: #075d55; color: #fff; border-radius: 6px; z-index: 2; }
#status { position: fixed; left: 12px; bottom: 70px; max-width: calc(100% - 48px); padding: 8px; background: #fff; white-space: pre-wrap; overflow-wrap: anywhere; }
.spacer { margin-top: 700px; }
</style>
</head>
<body>
<div id="toolbar"><button type="button" id="down">Go to 900px</button><button type="button" id="stop">Stop movement</button></div>
<h1 id="top" tabindex="-1">Top of the example page</h1>
<p>Use the fixed link after scrolling down.</p>
<p class="spacer">Middle of the page</p>
<p class="spacer">Lower content</p>
<a id="back" href="#top">Back to top</a>
<pre id="status" aria-live="polite"></pre>
<script>
$(function () {
const root = document.scrollingElement;
if (!root) return;
const $root = $(root);
let completed = 0, state = "ready";
function report() { $("#status").text(`scrollY=${Math.round(window.scrollY)}\ncompleted=${completed}\nstate=${state}`); }
function interrupt() {
if (state !== "moving") return;
$root.stop(true, false); state = "interrupted"; report();
}
$("#back").on("click.backTopLesson", function (event) {
event.preventDefault();
$root.stop(true, false);
state = "moving"; report();
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
$root.animate({ scrollTop: 0 }, { duration: reduce ? 0 : 600, complete: function () {
completed++; state = "done";
document.getElementById("top").focus({ preventScroll: true });
report();
} });
});
$("#down").on("click", function () { $root.stop(true, false).scrollTop(900); completed = 0; state = "ready"; report(); });
$("#stop").on("click", interrupt);
$(window).on("scroll.backTopLesson", report);
$(window).on("wheel.backTopLesson touchstart.backTopLesson", interrupt);
$(document).on("keydown.backTopLesson", function (event) {
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) interrupt();
});
report();
});
</script>
</body>
</html>
Initial output at the top follows. Rounding only makes displayed readings easier to read; it does not restrict animation calculations to integers.
scrollY=0
completed=0
state=ready
- Press Go to 900px. If the viewport is too tall to reach 900, reduce its height.
- Activate Back to top: normally it moves over 600ms. scrollY decreases through intermediate values, reaches zero and reports completed=1, state=done.
- The heading’s focus outline appears. Focus the lower link and use Enter to verify the same navigation.
- From 900 again, repeated clicks do not queue repeated journeys. The previous effect stops and the new one starts at the current position.
- Stop movement interrupts at an intermediate position with state=interrupted. Activate the link again or restore the comparison position with Go to 900px.
These actions, keyboard activation, repeated clicks, interruption and reduced motion were executed with 3.7.1 and 4.0.0 in Edge on Windows. A separate check with JavaScript disabled followed the #top anchor.
Repeated clicks, interruption and reduced motion
The .animate() target scrollTop is numeric zero. Without event.preventDefault(), native anchor navigation can conflict with animation. .stop(true, false) clears queued effects and stops the current one without completing it at the destination. Repeated clicks therefore restart from the current position without firing old completion callbacks.
Wheel, touch-start and scrolling keys call interrupt() so users can take over. It does not preventDefault on their scrolling. The Stop button uses the same function. An interrupted trip does not force focus to the top heading.
prefers-reduced-motion reports a preference for less nonessential motion. The example checks it on every activation, uses duration zero and follows the same focus-completion path. It does not change global jQuery effects settings.
focus({ preventScroll: true }) avoids a second position change after animation. A destination other than absolute top zero requires separate calculation of the actual target and fixed header offset. Changing only the id does not adapt the animation to every destination.
Apply it to an existing site
Check for an existing id="top" and keep link and target consistent. Scope selectors and namespaces to avoid overriding other handlers or CSS. The exercise’s 900px control, status output and artificial page height are not required on your real page.
Avoid applying CSS scroll-behavior:smooth and jQuery animation to the same movement. Here auto lets jQuery control duration. Where jQuery is unnecessary, native window.scrollTo() with smooth is an option; design completion and focus for that method.
If nothing moves, check available page scrolling, an inner-container layout and full jQuery loading. Review contrast, descriptive text, mobile hit area and overlapping fixed elements. Keep href as a usable anchor rather than javascript: or an empty address.




