【徹底解説】スムーススクロールを完全に制御する方法:React/Next.js編

目次

【React対応】ぬるっと動く!スムーススクロールを自作で完全制御する方法

「セクションリンクをクリックしたとき、ページを滑らかにスクロールさせたい」──そんな場面は、ブログやドキュメント系のページでよくありますよね。

本記事では、React(Next.js)環境で「完全に制御されたスムーススクロール」を実現する方法を、JavaScript(TypeScript)によるアニメーション制御をベースに紹介します。


✅ この記事で実現すること

  • 任意のセクションIDへのスムーススクロール
  • スクロール速度のカスタマイズ(ミリ秒指定)
  • 固定ヘッダーに隠れないスクロール位置調整
  • モバイル・PCの表示幅に応じた挙動の切り替え

✅ 実装コード:React用スムーススクロール

以下のコンポーネントを使えば、HTML内のセクションリンク(例: <a href="#section1">)を滑らかにスクロールさせることができます。

'use client'

import { useEffect } from 'react'
import parse from 'html-react-parser'

export default function TOCScroll({ content }: { content: string }) {
  useEffect(() => {
    const timeout = setTimeout(() => {
      const anchors = document.querySelectorAll('.toc_list a[href^="#"]')

      const handleClick = (e: Event) => {
        e.preventDefault()
        const href = (e.currentTarget as HTMLAnchorElement).getAttribute('href')
        if (!href) return

        const target = document.querySelector(href)
        if (target) {
          const isMobile = window.innerWidth < 768
          const offset = isMobile ? 60 : 180
          const targetY = target.getBoundingClientRect().top + window.scrollY - offset
          smoothScrollTo(targetY, 1000)
        }
      }

      anchors.forEach(anchor => anchor.addEventListener('click', handleClick))
      return () => {
        anchors.forEach(anchor => anchor.removeEventListener('click', handleClick))
      }
    }, 0)

    return () => clearTimeout(timeout)
  }, [content])

  return <>{parse(content)}</>
}

function smoothScrollTo(targetY: number, duration = 1000) {
  const startY = window.scrollY
  const diff = targetY - startY
  const startTime = performance.now()

  const easeInOutCubic = (t: number) =>
    t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2

  const step = (currentTime: number) => {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)
    const eased = easeInOutCubic(progress)
    window.scrollTo(0, startY + diff * eased)
    if (progress < 1) requestAnimationFrame(step)
  }

  requestAnimationFrame(step)
}

✅ どうして scrollIntoView を使わなかったの?

よくある方法として element.scrollIntoView({ behavior: 'smooth' }) もありますが、以下のような課題がありました:

  • 固定ヘッダーがあると対象要素が隠れる
  • スクロールの速さを調整できない
  • iOS Safari や一部環境で効かないことがある

そのため今回は、自作アニメーション(requestAnimationFrame)+イージング制御によって、より細かく・正確にコントロールしています。


✅ 一般的なスムーススクロールの手法比較

方法制御性安定性特徴
CSS scroll-behavior×簡単・全体適用に便利
scrollIntoView手軽だけど環境依存あり
scrollTo + offset固定ヘッダー対策も可能
requestAnimationFrame最高の柔軟性と自然な動き

✅ 補足:対象のHTMLが文字列な場合は?

この例では html-react-parser を使用して、HTML文字列を安全に描画しています。以下のように使用できます:

import parse from 'html-react-parser'

const html = '<h2 id="section1">見出し</h2>'

return <>{parse(html)}</>

✅ まとめ

スムーススクロールを自前で制御できるようになると、どんな構成のページでも自由自在にUXをデザインできるようになります。

特に、固定ヘッダーのあるページや、セクションジャンプを多用するドキュメント系のサイトでは、今回のような実装が大きな価値を発揮します。


📌 次のステップとしておすすめ:

  • 共通フック化して複数ページで再利用
  • スクロール完了後にアニメーションを追加
  • URLのハッシュを動的に反映(ナビゲーション強化)

ぜひあなたのプロジェクトにもこの「本気のスムーススクロール」を導入して、気持ちよく操作できるUIを作ってみてください!

この記事を書いた人

目次