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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Mastering Smooth Scrolling: CSS, JavaScript Methods and Edge‑Case Solutions

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. scroll-behavior: smooth; In documentation sites, the # anchor is often used to jump to a specific position, creating a tab‑switching effect.

CSS smooth scroll example
CSS smooth scroll example

To enable smooth scrolling, apply the following CSS to the scroll container:

.scroll-ctn {
  display: block;
  width: 100%;
  height: 300px;
  overflow-y: scroll;
  scroll-behavior: smooth;
  border: 1px solid grey;
}

The scroll-behavior: smooth property makes the default scroll animation smooth.

Compatibility

IE and iOS have poor support; a polyfill may be required.

Compatibility chart
Compatibility chart

Important notes

When scroll-behavior: smooth is set on a container, it overrides the JavaScript behavior: auto option, 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:

// First form
const x = 0, y = 200;
element.scrollTo(x, y);
// Second form
const options = { top: 200, left: 0, behavior: 'smooth' };
element.scrollTo(options);

The behavior option can be auto (instant) or smooth (animated).

Other APIs include setting scrollTop and scrollLeft directly, which have excellent compatibility:

// Set vertical scroll distance
container.scrollTop = 200;
// Set horizontal scroll distance
container.scrollLeft = 200;

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: smooth and a React useEffect hook:

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>
  );
}

The four possible ways to scroll a container are:

Assign scrollTop directly.

Call scrollTo with coordinates.

Call scrollTo with an options object.

Use scrollIntoView / scrollIntoViewIfNeeded on 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 vs script scroll demo
User vs script scroll demo

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 onScroll handler to differentiate the two.

Implementation

React example that tracks scroll state, distinguishes user and script scrolling, and provides a button to trigger script scrolling:

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>
  );
}

The utility listScroll encapsulates smooth scrolling with a fallback timeout to guarantee the callback execution:

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);
};

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptReactCSSscrollscrollIntoViewscrollTopsmooth scrolling
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.