WEBTODESIGN

スクロールイベントを使わずにページ内の現在地を表示するナビゲーションをJavaScriptで実装する

ページ内の現在地を表示するナビゲーションを実装したい。しかし、スクロールイベントだと記述が複雑かつ、処理数が多いことが気になってしまい、、、。

今回は、スクロールイベントは使わず、交差オブザーバーを利用して実装してみました。コードも短くてかなり満足な出来です!

コードの全貌

さて、最低限必要な部分のコードは以下です。CSSは省いています。

<!-- ナビゲーション部分 -->
<ul class="nav">
    <li><a href="#anker1">アンカー1</a></li>
    <li><a href="#anker2">アンカー1</a></li>
    <li><a href="#anker3">アンカー1</a></li>
</ul>
<!-- コンテンツ部分 -->
<div id="anker1"></div>
<div id="anker2"></div>
<div id="anker3"></div>
const observerEvent = () => {
    const navA = [...document.querySelectorAll('.nav a')];//ナビゲーションのaタグを全て取得
    const obA = navA.map( e => document.querySelector(e.hash));//取得したaタグのアンカーリンク部分を取得して配列にする
    const observeCallback = entries => {
        entries.forEach(e => {
            let i = obA.indexOf(e.target);
            if (e.isIntersecting)  navA[i].classList.add('active');//交差したら交差したaタグに「active」クラスを付与
            else navA[i].classList.remove('active');//交差してない場合aタグから「active」クラスを削除
        });
    }
    const observer = new IntersectionObserver(observeCallback, { rootMargin: '-50% 0px -50% 0px' });//rootMarginで交差の位置を設定
    obA.forEach(t => observer.observe(t));
};
observerEvent();

解説

HTMLを用意する

まず、ナビゲーション部分とコンテンツ部分のHTMLを用意します。 ナビゲーションからコンテンツの各部へ、アンカーリンクでジャンプする仕様です。

ポイントは、コンテンツの上部だけでなく全体で位置を把握する点です。 つまり、アンカーリンクをh2などに設置するのではなく、各コンテンツ全体にかけるということです。

(なので、ブログ記事などでは使えないと思います。 今度そちらも考えてみます。)

<!-- ナビゲーション部分 -->
<ul class="nav">
    <li><a href="#anker1">アンカー1</a></li>
    <li><a href="#anker2">アンカー1</a></li>
    <li><a href="#anker3">アンカー1</a></li>
</ul>
<!-- コンテンツ部分 -->
<div id="anker1"></div>
<div id="anker2"></div>
<div id="anker3"></div>

CSSは核心部ではないため省きます。ですが、ナビゲーションは画面に固定し、コンテンツの各部はある程度高さがあるとわかりやすいです。

また、今回は交差しているコンテンツに相対するaタグに「active」クラスをつけます。 なので、「active」クラスがついているaタグに現在地を示す装飾をつけておきましょう。

.nav a.active{
    /* 現在地を示す装飾 */
}

(JSでクラスを指定しているので、好きなクラス名で大丈夫です。 使用したクラス名を指定してください。)

JSで動かす

交差を検出するのには交差オブザーバーを使いました。

普段は要素が画面に入ったときのアニメーションに使うことが多いです。そして、今回はそれを応用してみました。

※交差オブザーバーについて詳しくはMDNドキュメントをご確認ください。
mdn web docsの交差オブザーバーAPIのページ

const observerEvent = () => {
    const navA = [...document.querySelectorAll('.nav a')];//ナビゲーションのaタグを全て取得
    const obA = navA.map( e => document.querySelector(e.hash));//取得したaタグのアンカーリンク部分を取得して配列にする
    const observeCallback = entries => {
        entries.forEach(e => {
            let i = obA.indexOf(e.target);
            if (e.isIntersecting) navA[i].classList.add('active');//交差したら交差したaタグに「active」クラスを付与
            else navA[i].classList.remove('active');//交差してない場合aタグから「active」クラスを削除
        });
    }
    const observer = new IntersectionObserver(observeCallback, { rootMargin: '-50% 0px -50% 0px' });//rootMarginで交差の位置を設定
    obA.forEach(t => observer.observe(t));
};
observerEvent();

これは、簡単にいうと「現在地のナビゲーションの項目にactiveクラスをつけて見た目を変える」という処理です。

要素の取得

まずは、ナビゲーションのaタグを全て取得して配列にします。 そして、そのアンカーリンクを拾い、idがついた要素を新しい配列にします。

const navA = [...document.querySelectorAll('.nav a')];
const obA = navA.map( e => document.querySelector(e.hash));

mapメソッドは、配列のそれぞれの値に処理を施してその値を新しい配列に返せる便利な関数です。また、.hashでアンカーリンクを取得しています。

コンテンツの監視と検出の位置

次に、交差オブザーバーでそれぞれのコンテンツを監視します。今回監視するのは、obAの配列の中身です。

const observeCallback = entries => {
    entries.forEach(e => {
        //監視中の処理
    });
}
const observer = new IntersectionObserver(observeCallback, { rootMargin: '-50% 0px -50% 0px' });
obA.forEach(t => observer.observe(t));

これは、次の項目が画面のちょうど中央を過ぎると、activeクラスが移動する設定です。

初期設定ではビューポートに入ったか入ってないかを監視します。 オプションのrootMarginで、その領域を指定できます。

以下の部分です。

rootMargin: '-50% 0px -50% 0px'

今回は上下を-50%しています。

マイナス値は内側に狭まる値なので、ちょうど画面の中央に一本線が弾かれているイメージです。そして、そこを通っているか通っていないかを検出します。

配列の番号で照らし合わせる

次に、コンテンツが交差している部分(e.target)が配列(obA)の何番目にあたるかを検出し、その数値をiに代入します。

let i = obA.indexOf(e.target);

indexOfメソッドは配列の検索を行える便利な関数です。

そして、aタグ配列のi番目の要素のクラスの付け替えをします。

2つの配列の中身は相対しています。 なので、コンテンツ配列の0番目と交差しているときは、aタグ配列の0番目にアクティブなクラス名をつければいいということです。

if (e.isIntersecting) navA[i].classList.add('active');
else navA[i].classList.remove('active');

if (e.isIntersecting) 〜は「交差しているとき」の処理です。 それ以外(else)は交差していないときの処理。 「active」クラスをつけ外ししています。

おさらい

コードの全体像をおさらいです。

<!-- ナビゲーション部分 -->
<ul class="nav">
    <li><a href="#anker1">アンカー1</a></li>
    <li><a href="#anker2">アンカー1</a></li>
    <li><a href="#anker3">アンカー1</a></li>
</ul>
<!-- コンテンツ部分 -->
<div id="anker1"></div>
<div id="anker2"></div>
<div id="anker3"></div>
const observerEvent = () => {
    const navA = [...document.querySelectorAll('.nav a')];
    const obA = navA.map( e => document.querySelector(e.hash));
    const observeCallback = entries => {
        entries.forEach(e => {
            let i = obA.indexOf(e.target);
            if (e.isIntersecting) navA[i].classList.add('active');
            else navA[i].classList.remove('active');
        });
    }
    const observer = new IntersectionObserver(observeCallback, { rootMargin: '-50% 0px -50% 0px' });
    obA.forEach(t => observer.observe(t));
};
observerEvent();

さて、交差オブザーバーはスクロールイベントよりも処理が最適で推奨されているだけでなく、複雑な記述が不要なのもいいですね!

ブログ記事の目次に使うにはもう少し工夫が必要ですが、通常のWebページには使いやすそうです。

というか使いました、よきです。