Mastering Smooth Scrolling: CSS, JavaScript Methods and Edge‑Case Solutions
This article explores how to implement smooth scrolling using CSS and JavaScript, compares native scroll APIs, demonstrates practical React examples, explains how to differentiate user‑initiated and script‑driven scrolling, and provides compatibility tips and workarounds for common pitfalls.
Introduction
Implementing list scrolling interactions can become complex; this article discusses practical uses of scroll, including CSS smooth scrolling, JavaScript scroll methods, and distinguishing user‑initiated from script‑driven scrolling.
CSS Smooth Scrolling
One‑line style improvement
Adding
scroll-behavior: smooth;to a scrollable container improves the user experience in frequent scrolling scenarios.
<code>scroll-behavior: smooth;</code>In documentation sites, the
#anchor is often used to jump to a specific position, creating a tab‑switching effect.
To enable smooth scrolling, apply the following CSS to the scroll container:
<code>.scroll-ctn {
display: block;
width: 100%;
height: 300px;
overflow-y: scroll;
scroll-behavior: smooth;
border: 1px solid grey;
}</code>The
scroll-behavior: smoothproperty makes the default scroll animation smooth.
Compatibility
IE and iOS have poor support; a polyfill may be required.
Important notes
When
scroll-behavior: smoothis set on a container, it overrides the JavaScript
behavior: autooption, so immediate scrolling will not work.
The property also affects the browser’s Ctrl+F search behavior, making it smooth.
JavaScript Scroll Methods
Basic methods
scrollTo : scroll to a target position.
scrollBy : scroll relative to the current position.
scrollIntoView : bring an element into the viewport.
scrollIntoViewIfNeeded : bring an element into view only if it is outside.
Two calling forms of
scrollTo:
<code>// First form
const x = 0, y = 200;
element.scrollTo(x, y);
// Second form
const options = { top: 200, left: 0, behavior: 'smooth' };
element.scrollTo(options);</code>The
behavioroption can be
auto(instant) or
smooth(animated).
Other APIs include setting
scrollTopand
scrollLeftdirectly, which have excellent compatibility:
<code>// Set vertical scroll distance
container.scrollTop = 200;
// Set horizontal scroll distance
container.scrollLeft = 200;</code>Applications
Typical scenarios:
Component initialization – scroll to a target.
Clicking a bottom‑page element – trigger page‑turn scrolling.
…
Example: after a list component mounts, automatically scroll to the third item using
scroll-behavior: smoothand a React
useEffecthook:
<code>import React, { useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const listRef = useRef({ cnt: undefined, items: [] });
const listItems = ["A", "B", "C", "D"];
useEffect(() => {
// Scroll to third item
listRef.current.items[2].scrollIntoView();
}, []);
return (
<div className="App">
<ul className="list-ctn" ref={ref => (listRef.current.cnt = ref)}>
{listItems.map((item, index) => (
<li className="list-item" ref={ref => (listRef.current.items[index] = ref)} key={item}>
{item}
</li>
))}
</ul>
</div>
);
}
</code>The four possible ways to scroll a container are:
Assign
scrollTopdirectly.
Call
scrollTowith coordinates.
Call
scrollTowith an options object.
Use
scrollIntoView/
scrollIntoViewIfNeededon an element.
Distinguishing User Scroll from Script Scroll
Background
In a subtitle‑synchronization scenario, automatic paging should stop when the user manually scrolls, and a “return to current position” button should resume automatic paging.
User scroll
Typical user scroll actions include mouse wheel, arrow keys, page‑up/down, etc.
Script scroll
Any scroll triggered by code is considered script scroll. A flag can be set on the
onScrollhandler to differentiate the two.
Implementation
React example that tracks scroll state, distinguishes user and script scrolling, and provides a button to trigger script scrolling:
<code>import throttle from "lodash.throttle";
import React, { useRef, useState } from "react";
import { listScroll } from "./utils";
import "./styles.css";
export default function App() {
const [wording, setWording] = useState("Waiting");
const cacheRef = useRef({ isScriptScroll: false, cnt: null, scrollTop: 0 });
const onScroll = throttle(() => {
if (cacheRef.current.isScriptScroll) {
setWording("Script scrolling");
} else {
cacheRef.current.scrollTop = cacheRef.current.cnt.scrollTop;
setWording("User scrolling");
}
}, 200);
const scriptScroll = () => {
cacheRef.current.scrollTop += 600;
cacheRef.current.isScriptScroll = true;
listScroll(cacheRef.current.cnt, cacheRef.current.scrollTop, () => {
setWording("Script scroll finished");
cacheRef.current.isScriptScroll = false;
});
};
return (
<div className="App">
<button className="btn" onClick={() => scriptScroll()}>Trigger script scroll</button>
<p className="wording">Current state: {wording}</p>
<ul className="list-ctn" onScroll={onScroll} ref={ref => (cacheRef.current.cnt = ref)}>
{Array.from({ length: 1000 }, (_, i) => i + 1).map(item => (
<li className="list-item" key={item}>{item}</li>
))}
</ul>
</div>
);
}
</code>The utility
listScrollencapsulates smooth scrolling with a fallback timeout to guarantee the callback execution:
<code>export const listScroll = (element, targetPos, callback) => {
const { scrollHeight: listHeight } = element;
if (targetPos < 0 || targetPos > listHeight) return callback?.();
element.scrollTo({ top: targetPos, left: 0, behavior: "smooth" });
if (!callback) return;
if (Math.abs(element.scrollTop - targetPos) <= 10) return callback();
setTimeout(() => callback(), 1000);
};
</code>Conclusion
The article introduced native scroll APIs such as
scrollIntoView, highlighted common pitfalls, and provided a reference implementation for distinguishing user scroll from script scroll. Smooth scrolling may appear trivial, but it often hides considerable complexity that developers should consider during design and implementation.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.