TL;DR
このページでは、スクロールの途中でメニューを固定する実装方法について解説しますね。ウェブページをスクロールしても特定のメニューだけは常に画面上部に表示され続ける機能を Next.js と TypeScript で実装します。
開発環境 | バージョン |
---|---|
Next.js | 13.4.4 |
TypeScript | 5.0.4 |
Emotion | 11.11.0 |
React | 18.2.0 |
スクロールの途中でメニューを固定する
ウェブサイトを利用していると、特定の位置までスクロールした時点でメニューバーやヘッダーが画面上部に固定され、その後スクロールしても画面上部に表示され続けるという現象を目にすることがあるでしょう。これはユーザー体験を高めるための一つの手法であり、ユーザーがいつでもナビゲーションメニューにアクセスできるようにすることで、サイトの利用がよりスムーズになります。
では、基本的な実装方法について解説します。
import { useState, useEffect } from 'react'
// Props型定義。固定化するスクロール位置を示すthresholdを受け取る。
type Props = {
threshold: number
}
const StickyMenu = ({ threshold }: Props) => {
// isStickyというstateを定義。デフォルトはfalse(非固定状態)
const [isSticky, setIsSticky] = useState(false)
useEffect(() => {
// スクロールイベントのハンドラー定義。
// スクロール位置がthresholdを超えるとisStickyをtrueに設定
const handleScroll = () => {
const currentScrollY = window.scrollY // 現在のスクロール位置を取得
setIsSticky(currentScrollY > threshold) // 現在位置が閾値を超えていればisStickyをtrueに
}
// スクロールイベントにhandleScrollを登録
window.addEventListener('scroll', handleScroll)
// コンポーネントアンマウント時にイベントリスナーを削除
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [threshold]) // thresholdが変更されたらuseEffectを再実行
// isStickyがtrueならpositionをfixedにし、
// そうでなければrelativeに設定
return <div style={{ position: isSticky ? 'fixed' : 'relative' }}>{/* Menu content */}</div>
}
ここでは、StickyMenu
というコンポーネントを作成しています。このコンポーネントはthreshold
というプロッパティを受け取り、この値を基準にしてスクロール位置を判断します。
threshold
よりもスクロール位置が下だった場合、isSticky
をtrue
に設定します。そして、isSticky
がtrue
の時はメニューの位置をfixed
に設定し、それ以外の時はrelative
に設定します。これにより、特定のスクロール位置を超えたらメニューが固定されるようになります。
Next.js で、「スクロールの途中でメニューを固定する」を実装
次に、このStickyMenu
コンポーネントを Next.js のプロジェクトで使ってみましょう。
pages/index.tsx
import { StickyMenu } from '../components/StickyMenu' // StickyMenuコンポーネントのインポート
// IndexPageというReactコンポーネントの定義
const IndexPage = () => (
<>
{/* StickyMenuコンポーネントの利用。スクロール位置が200を超えた時にメニューを固定化 */}
<StickyMenu threshold={200}>
{/* メニューアイテムのリスト */}
<ul>
<li>
<a href="#section1">Section 1</a>
</li>
<li>
<a href="#section2">Section 2</a>
</li>
</ul>
</StickyMenu>
</>
)
// IndexPageコンポーネントをエクスポート
export default IndexPage
この例では、StickyMenu
をインポートし、その子要素としてメニュー項目を配置しています。また、threshold
プロパティに 200 を指定していますので、200px スクロールした時点でメニューが画面上部に固定されます。
メニューのスタイルを追加
次に、メニューにスタイルを追加しましょう。ここでは Emotion を使って CSS-in-JS のスタイリングを行います。
/** @jsxImportSource @emotion/react */
// @emotion/reactとcssをインポート
import { css } from '@emotion/react'
import { useState, useEffect } from 'react' // ReactのuseState, useEffectをインポート
type Props = {
threshold: number // thresholdプロパティを定義
}
// StickyMenuというコンポーネントの定義
const StickyMenu = ({ threshold }: Props) => {
// isStickyというstateを定義、初期値はfalse
const [isSticky, setIsSticky] = useState(false)
// useEffectフックを使用。
// スクロール位置がthresholdを超えたときにisStickyをtrueにする
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY // 現在のスクロール位置を取得
setIsSticky(currentScrollY > threshold) // スクロール位置がthresholdを超えているかチェック
}
// スクロールイベントリスナーを設定
window.addEventListener('scroll', handleScroll)
// コンポーネントアンマウント時にイベントリスナーを削除
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [threshold]) // thresholdが変更されたときにeffectを再実行
// メニューのスタイルを定義。isStickyの値に基づいてスタイルを切り替え
const menuStyle = css`
position: ${isSticky ? 'fixed' : 'relative'};
top: 0;
width: 100%;
background: white;
box-shadow: ${isSticky ? '0 2px 4px rgba(0, 0, 0, 0.2)' : 'none'};
`
// コンポーネントをレンダリング。スタイルはmenuStyleを適用
return <div css={menuStyle}>{/* Menu content */}</div>
}
ここでは、menuStyle
という変数を用意し、その中にスタイルを定義しています。isSticky
がtrue
の時はposition
をfixed
にし、top
を 0 にすることでメニューを画面上部に固定しています。また、isSticky
がtrue
の時だけbox-shadow
を適用することで、固定されたメニューに影をつけています。
メニュー項目のスクロール先を指定
最後に、各メニュー項目がクリックされた時に該当するセクションにスクロールするようにします。
const StickyMenu = ({ threshold }: Props) => {
// omitted...
const handleItemClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault()
const targetId = event.currentTarget.getAttribute('href')
const targetElement = document.querySelector(targetId)
if (targetElement) {
window.scrollTo({
top: targetElement.getBoundingClientRect().top + window.pageYOffset - 100,
behavior: 'smooth',
})
}
}
return (
<div css={menuStyle}>
<ul>
<li>
<a href="#section1" onClick={handleItemClick}>
Section 1
</a>
</li>
<li>
<a href="#section2" onClick={handleItemClick}>
Section 2
</a>
</li>
{/* More menu items */}
</ul>
</div>
)
}
ここでは、各<a>
要素のonClick
イベントにhandleItemClick
関数を設定しています。この関数は、クリックされた要素のhref
属性を取得し、それが指す要素にスクロールします。window.scrollTo
メソッドを使ってスクロールを行い、top
オプションに目標の要素の位置を指定しています。また、-100
を追加してメニューの高さを考慮し、behavior
オプションに'smooth'
を指定することでスムーズなスクロールを実現しています。
以上のように、Next.js と TypeScript を使用することで、「スクロールの途中でメニューを固定する」機能を簡単かつ効率的に実装することができます。