The whole feature is one line. Put this in your stylesheet and every in-page anchor jump animates instead of snapping:
html {
scroll-behavior: smooth;
}That is it. Click a link to #section-3, or hit the back button to a hash, and the page glides there. No jQuery animate(), no scroll-easing library, no event.preventDefault() on every anchor. The scroll-behavior property is Baseline Widely Available: Safari was the last engine to ship it (15.4, March 2022), and it crossed the Baseline "widely available" threshold in September 2024. Every current browser supports it, so the plugins people used to reach for (the old jQuery and MooTools smooth-scroll scripts) are now dead weight you can delete.
What the one line actually controls
scroll-behavior changes how a scroll is animated, not whether scrolling happens. It applies to programmatic and navigation scrolls: clicking an href="#id" link, a hash change in the URL, and the JavaScript scroll APIs. It deliberately does not affect the user dragging the scrollbar or spinning the mouse wheel, that stays as responsive as ever. So you get animated jumps for "skip to section" links without making the whole page feel laggy under the user's own hand.
The values are just auto (the default, instant jump) and smooth (animated). The easing curve and duration are picked by the browser; there is no CSS knob to tune them, which is the one real limitation. If you need a specific 600ms ease-out, that is still a JavaScript job.
Per-element vs global
Setting it on html covers the document, which is what you want for a typical page of anchor links. But scroll-behavior works on any scroll container, so you can animate just one independently scrolling box and leave the rest of the page alone:
/* Smooth only inside this panel's own scrollbar */
.chat-log {
overflow-y: auto;
scroll-behavior: smooth;
}When a JS call scrolls .chat-log to the bottom on a new message, it eases down instead of teleporting, while the main document keeps its default instant behavior.
The sticky-header gotcha: scroll-margin-top
Here is the bug everyone hits. You add a sticky header, you add smooth scroll, and now every anchor jump lands with the target heading hidden behind the header. The browser scrolls the target to the very top of the viewport, and your fixed bar is sitting on top of it.
The fix is scroll-margin-top on the scroll targets. It tells the browser to stop short by that much when snapping an element into view:
:root {
--header-height: 4rem;
}
/* Offset any element an anchor link can target */
:target {
scroll-margin-top: var(--header-height);
}
/* Or scope it to the headings you actually link to */
h2[id],
h3[id] {
scroll-margin-top: 4.5rem;
}scroll-margin-top is the clean, modern answer. It replaces the old hacks: invisible padding, a negative-margin pseudo-element, or an empty offset anchor <span> before each heading. Set the value a little larger than the header so the heading clears it with breathing room. It is well supported across current browsers, and the shorthand scroll-margin covers all four sides if you ever scroll horizontally too.
JS-triggered scrolls: scrollIntoView
When the scroll comes from your own code rather than an anchor click (scrolling to a freshly added item, a validation error, a search result), the JavaScript equivalent is scrollIntoView with the behavior option:
document.querySelector("#section-3").scrollIntoView({
behavior: "smooth",
block: "start",
});Two things worth knowing. First, scrollIntoView respects scroll-margin-top too, so the sticky-header offset you set in CSS carries over for free. Second, if html already has scroll-behavior: smooth, even a plain scrollIntoView() with no options animates, because the property governs the element's programmatic scrolls. Passing behavior: "smooth" explicitly is the clearer choice when you do not want to depend on a global rule.
Respect prefers-reduced-motion (do not skip this)
Smooth scrolling is motion, and a long animated jump down a page can trigger nausea or dizziness for people with vestibular disorders. The fix is to gate the animation behind a media query, so users who have asked their OS for reduced motion get the instant jump instead:
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}Note the inversion. Rather than turning smooth scroll off for the reduced-motion crowd, default to instant and only opt in to smooth when the user has expressed no-preference. That way the safe behavior is the fallback, and a browser that never reports a preference still gets a sane default. This is the correct accessibility pattern for any motion you add, and there is a fuller treatment in respecting prefers-reduced-motion.
Putting it together
A complete, production-safe smooth-scroll setup is three short rules:
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
:target {
scroll-margin-top: 4.5rem; /* clear the sticky header */
}That covers anchor links, the hash-on-load case, the sticky-header offset, and motion-sensitive users, with zero JavaScript and nothing to install. Reach for scrollIntoView({ behavior: "smooth" }) only where the scroll is something your code initiates.
FAQ
Yes, completely. html { scroll-behavior: smooth } animates every in-page anchor link and hash navigation with no script at all. JavaScript only enters the picture when you want to trigger a scroll yourself, in which case you use scrollIntoView({ behavior: 'smooth' }).
The browser scrolls the target to the very top of the viewport, and your fixed header sits over it. Add scroll-margin-top to the scroll targets (a value a little larger than the header height) so the browser stops short and the heading clears the bar.
Not in CSS. The duration and easing curve are chosen by the browser, and there is no property to override them. If you need a specific speed or curve, animate the scroll in JavaScript instead.
Yes. It reached Baseline Widely Available in September 2024 (Safari 15.4 in March 2022 was the last engine to ship it), and it is supported in every current browser. Older browsers that do not understand it simply fall back to an instant jump, which is a graceful degradation, so there is no reason to hold back.
No. The one CSS line replaces the old jQuery and MooTools smooth-scroll plugins for anchor navigation. Delete them. JavaScript is only worth keeping for scrolls your own code initiates or when you genuinely need a custom duration the browser will not give you.
See also
- Respecting users with prefers-reduced-motion: the accessibility media query that gates smooth scroll, and the rest of your animations, for motion-sensitive users.
- The CSS :has() parent selector: another modern, JavaScript-killing CSS feature that is now Baseline and production-safe.
- Doing math in CSS with calc(): pair it with scroll-margin-top to derive a header offset from your spacing variables instead of hardcoding a pixel value.
Sources
Authoritative references this article was fact-checked against.
- scroll-behavior - CSS, MDN Web Docsdeveloper.mozilla.org
- scroll-margin-top - CSS, MDN Web Docsdeveloper.mozilla.org
- Element.scrollIntoView() - MDN Web Docsdeveloper.mozilla.org
- CSS scroll-behavior - Can I use browser support tablescaniuse.com





