How to Create Smooth Scrolling with CSS, JavaScript, and jQuery
Make in-page navigation scroll smoothly with CSS, JavaScript, or jQuery. Choose the right scroll container, support reduced motion, and handle fixed headers.
For ordinary in-page links, add scroll-behavior: smooth to the page’s scrolling box. Use JavaScript’s scrollIntoView() when a button or dynamic interaction chooses a target or alignment. Use jQuery’s .animate() when your project already uses jQuery and needs a chosen duration or easing curve. In every case, make sure you target the element that actually scrolls: the viewport or a nested overflow panel.
1. CSS: smooth scrolling for anchor links
Keep navigation as a regular link with a matching target ID. The browser updates the URL fragment and can navigate to the target even when JavaScript is unavailable.
<nav aria-label="On this page">
<a href="#features">Features</a>
</nav>
<main>
<section id="features">
<h2>Features</h2>
<p>Section content…</p>
</section>
</main>
html {
scroll-behavior: smooth;
}
/* Prevent a fixed header from covering the destination. */
section[id] {
scroll-margin-top: 5rem;
}
/* Honor the visitor's operating-system motion preference. */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
scroll-behavior applies to a scrolling box when scrolling is triggered by navigation or CSSOM scrolling APIs. Set it on the box that moves. For viewport scrolling, use the root element as above. CSS smooth scrolling has a user-agent-defined duration and easing; browsers do not promise one fixed speed. The property itself is not animatable. See MDN’s scroll-behavior reference.
Nested scroll panels
If a panel scrolls independently, set the property on that panel, not just on html. Give the panel a constrained height and overflow so it has a scroll range.
.toc-panel {
height: 24rem;
overflow-y: auto;
scroll-behavior: smooth;
}
.toc-panel [id] {
scroll-margin-top: 1rem;
}
Anchor navigation within that panel can then scroll the panel. If the document itself is the scroller, configuring a nested panel will not affect page navigation.
2. JavaScript: choose a target and alignment
Element.scrollIntoView() handles the movement for you; it does not require a custom animation loop.
const target = document.querySelector("#features");
if (target) {
target.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest"
});
}
The behavior option accepts smooth, instant, or auto. auto follows the computed scroll-behavior. The block alignment can be start, center, end, or nearest; inline controls horizontal alignment. A target’s scroll-margin is usually the cleanest way to account for a fixed header.
const target = document.querySelector("#features");
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (target) {
target.scrollIntoView({
behavior: reduceMotion ? "instant" : "smooth",
block: "start"
});
}
Prefer a real anchor when the action is navigation. Use a button when it performs an action, such as opening a panel and then moving to a target. This preserves keyboard behavior and expected link semantics.
Scroll to coordinates
For a known coordinate rather than an element, the window and scrollable elements provide scrollTo() and scroll() with a behavior option.
// Move the document viewport.
window.scrollTo({ top: 600, behavior: "smooth" });
// Move a nested scrolling element.
const panel = document.querySelector(".toc-panel");
panel?.scrollTo({ top: 300, behavior: "smooth" });
Prefer scrollIntoView() when the destination is an element: it avoids maintaining a coordinate that can become stale when content or layout changes.
3. jQuery: animate scrollTop
jQuery can animate the non-style property scrollTop. The familiar page-scrolling pattern is:
$("html, body").animate({
scrollTop: $("#features").offset().top
}, 500);
This uses a 500-millisecond duration. jQuery’s default duration is 400 milliseconds and its default easing is swing. The built-in easing choices are swing and linear; other easing functions require an easing plugin. See the jQuery .animate() API.
For a nested panel, animate that panel’s scroll position instead of html, body:
const $panel = $(".toc-panel");
const targetTop = $("#features").position().top + $panel.scrollTop();
$panel.animate({ scrollTop: targetTop }, 500, "swing");
.scrollTop() reads or sets the current vertical position. An element that is not scrollable reports zero. The example computes a panel-relative destination; check the panel’s positioning and target structure in your own layout. Do not add jQuery just to make basic anchor links smooth when native CSS or browser APIs meet the requirement.
4. Choosing the right method
| Method | Best for | Control | Dependency |
|---|---|---|---|
CSS scroll-behavior |
Ordinary anchor navigation | Browser chooses timing and easing | None |
scrollIntoView() |
Dynamic targets and explicit alignment | Choose behavior and alignment; browser controls smooth timing | None |
jQuery .animate() |
Existing jQuery projects needing a duration/easing option | Duration and available easing functions | jQuery, plus an easing plugin for additional easing |
There is no universally smoothest option. CSS smooth scrolling and native smooth scrolling use browser-defined timing. Choose based on the interaction, the scroll container, motion preferences, and whether explicit duration control matters.
5. Accessibility and interaction details
- Honor reduced motion. Use
@media (prefers-reduced-motion: reduce)for CSS andmatchMedia()when choosing behavior in JavaScript. The preference reflects a visitor’s system-level motion setting. See MDN’s reduced-motion reference. - Keep links as links. Use
<a href="#target">for navigation so keyboard users and visitors without JavaScript retain expected behavior. - Check focus separately. Scrolling does not necessarily move keyboard focus. For navigation that changes the main content context, decide whether focus should move too, and implement that deliberately without making every ordinary table-of-contents click unexpectedly disruptive.
- Account for sticky or fixed headers. Set an appropriate
scroll-margin-topon targets. If header height changes across breakpoints, use responsive values. - Do not promise a fixed native duration. CSS and native smooth scrolling do not expose a duration setting.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Anchor jumps instead of scrolling smoothly | scroll-behavior is missing, overridden, or applied to the wrong scrolling box. |
Inspect computed styles on the viewport root or the nested panel that actually scrolls. Check whether reduced-motion styling intentionally sets it to auto. |
| Target lands behind the header | The destination aligns at the top edge while a fixed header overlays it. | Add scroll-margin-top to target elements and size it for the header. |
| JavaScript reports a null target | The selector does not match, the ID differs, or code runs before the target exists. | Check the selector and ID, and run after the DOM is available. Guard with an existence check as in the examples. |
| The page does not move when a panel should scroll | The code scrolls the window while a nested element owns the overflow, or vice versa. | Call scrollTo() on the panel or animate its scrollTop. Confirm it has a constrained size, overflow, and scrollable content. |
| jQuery animation goes to the wrong position | The document offset was used for a nested panel, or layout changed after calculating the position. | Calculate in the container’s coordinate space, use the panel’s current scroll position, and recalculate after content/layout changes. |
| Scrolling is instant for some visitors | Their reduced-motion preference or site CSS disables smooth motion. | Check the active media query and computed style. An instant path for reduced motion is expected. |
| Two animations compete | Repeated clicks start overlapping jQuery animations or application code issues multiple scroll calls. | Prevent duplicate triggers, or stop the existing jQuery animation before starting another when that matches the interaction. |
7. Performance, reliability, and cost
For ordinary navigation, CSS and native browser methods avoid an added library and a hand-built animation loop. jQuery is reasonable when the application already includes it or specifically needs its animation API. Avoid repeatedly measuring layout and issuing scroll commands during every frame; use the native scrolling methods for element targets. None of these APIs guarantees identical timing across browsers for smooth behavior, so verify the target browser and device matrix when exact motion is a product requirement. No special infrastructure cost is required for these browser features; the main dependency cost is shipping jQuery solely for this effect.
8. Or skip the browser setup
If your actual task is capturing a rendered page, ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. One GET request returns an image or PDF; see the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = await res.arrayBuffer();
- Cookie banners are accepted and removed, along with known consent platforms, newsletter popups, and chat widgets; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdffor Claude, Cursor, and other MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
9. FAQ
Does CSS smooth scrolling let me set the animation duration?
No. The browser chooses the duration and easing. Use jQuery’s animation API if your existing project needs an explicit duration, or accept the native behavior.
Will scrollIntoView() always scroll the whole page?
It scrolls ancestor scrolling boxes as needed to bring the target into view. If you need a particular container or coordinate behavior, call that container’s scrolling method directly.
Should I use jQuery for a new page just for smooth scrolling?
Usually not. Native CSS handles regular anchor links, and scrollIntoView() handles many dynamic targets without adding a dependency.
Can I keep smooth scrolling while respecting reduced motion?
Yes. Enable it by default and switch to automatic or instant behavior when prefers-reduced-motion: reduce matches.


