BLOG

Vanilla JSで追従ナビゲーションを作ってみた!

Written by uchida

INDEX

説明

Vanilla JS(ライブラリやフレームワークを使用しない、シンプルなJavaScript)とHTML、CSSのみを使った、追従ナビゲーションを作成します。

追従の挙動に加え、

  • MV(メインビジュアル)とフッターに近づいた際は、CSSのtransformプロパティを使って非表示にする
  • 画面に表示されているsectionに応じて、追従ナビゲーションのテキストカラーが自動で変わる

といった仕様を追加しようと思います。

今回は、scrollイベントを使った追従ナビゲーション、Intersection Observerを使った追従ナビゲーションの2パターンで作成してみました。

完成版1(scroll イベントでの実装)

完成版2(Intersection Observerでの実装)

実装手順 HTML / CSS

まずはマークアップから準備します。
マークアップはscrollイベントを使用した例、Intersection Observerを使用した例ともに同じものを使用します。

手順1(HTML / ページの枠組作成)

とあるコーポレートサイトのTopページと仮定し、MV、フッターを配置します。sectionタグにはそれぞれid属性を付与します。

  <body>
    <header class="mv">
      <h1>MV</h1>
    </header>
    <main>
      <section class="about" id="about">
        <h2>ABOUT</h2>
      </section>
      <section class="service" id="service">
        <h2>SERVICE</h2>
      </section>
      <section class="topics" id="topics">
        <h2>TOPICS</h2>
      </section>
      <section class="recruit" id="recruit">
        <h2>RECRUIT</h2>
      </section>
      <section class="news" id="news">
        <h2>NEWS</h2>
      </section>
      <section class="contact" id="contact">
        <h2>CONTACT</h2>
      </section>
      <footer id="footer">
      </footer>
    </main>
  </body>

手順2(HTML / 追従ナビゲーション部分作成)

追従ナビゲーションのHTMLを記述します。
追従ナビゲーションの各aタグには、手順1でsectionタグに付与したid属性と同じid名のhref属性を付与します。
また、初期ロード時にハイライト表示を適用しておきたい追従ナビゲーションのaタグに、is-currentというclass名を追加しておきます。

<body>
  <header class="mv">
    <h1>MV</h1>
  </header>
  <main>
    <nav id="page-nav" class="page-nav">
      <ul class="page-nav__list">
        <li class="page-nav__item">
          <a class="page-nav__link is-current" href="#about">
            <span class="page-nav__text">ABOUT</span>
          </a>
        </li>
        <li class="page-nav__item">
          <a class="page-nav__link" href="#service">
            <span class="page-nav__text">SERVICE</span>
          </a>
        </li>
        <li class="page-nav__item">
          <a class="page-nav__link" href="#topics">
            <span class="page-nav__text">TOPICS</span>
          </a>
        </li>
        <li class="page-nav__item">
          <a class="page-nav__link" href="#recruit">
            <span class="page-nav__text">RECRUIT</span>
          </a>
        </li>
        <li class="page-nav__item">
          <a class="page-nav__link" href="#news">
            <span class="page-nav__text">NEWS</span>
          </a>
        </li>
        <li class="page-nav__item">
          <a class="page-nav__link" href="#contact">
            <span class="page-nav__text">CONTACT</span>
          </a>
        </li>
      </ul>
    </nav>
    <section class="about" id="about">
      <h2>ABOUT</h2>
    </section>
    <section class="service" id="service">
      <h2>SERVICE</h2>
    </section>
    <section class="topics" id="topics">
      <h2>TOPICS</h2>
    </section>
    <section class="recruit" id="recruit">
      <h2>RECRUIT</h2>
    </section>
    <section class="news" id="news">
      <h2>NEWS</h2>
    </section>
    <section class="contact" id="contact">
      <h2>CONTACT</h2>
    </section>
    <footer id="footer">
    </footer>
  </main>
</body>

手順3(CSS / スタイル調整)

追従ナビゲーションにposition: fixed;を付与し、画面上部に固定します。
X軸(横方向の位置)は画面に対し、中央になるよう、transform: translateX(-50%);を付与します。
また、追従ナビゲーションを隠すためのclassとして、今回は.is-hiddenというclass名を、表示されているセクションに応じてテキストカラーが変化するためのclass名として、.is-currentを用意します。

.page-nav{
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 1080px;
  z-index: 100;
  margin-inline: auto;
  transition-duration: 0.3s;
  &__list{
    padding: 15px 45px;
    margin-inline: auto;
    list-style-type: none;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 30px;
  }
  &__link {
    color: #000;
    text-decoration: none;
  }
}

.is-hidden {
  opacity: 0;
  transform: translateX(-50%) translateY(-50%);
  transition-duration: 0.3s;
}

.is-current {
  color: red;
  position: relative;
}

※SCSS記法を使用しています。

実装手順 JavaScript (scrollイベント編)

手順4(JavaScript / 追従ナビゲーションを隠す処理)

JavaScriptのscrollイベントを使って、追従ナビゲーションを隠す挙動を実装します。
scrollイベントの関数内で、ページナビゲーション、フッター(追従ナビゲーションを隠すトリガー)、idaboutがついたsectionタグ(追従ナビゲーションを表示させるトリガー)をローカル変数として宣言します。
また、if (!pageNav || !footer || !firstSection || navLinks.length === 0) return; で、いずれかの要素がページ上に存在しない場合は処理自体を実行しないようにしておきます。

document.addEventListener("DOMContentLoaded", () => {
  const pageNav = document.getElementById('page-nav');
  const footer = document.getElementById('footer');
  const firstSection = document.getElementById('about');
  const navLinks = document.querySelectorAll(".page-nav__link");
 
 // 対象の要素が存在しない場合は処理をスキップ
  if (!pageNav || !footer || !firstSection || navLinks.length === 0) return;
});

updateNavVisibility関数内で、フッターからの距離が500px、または、最初のsectionからの距離が0pxの場合、追従ナビゲーションに対して、CSSのclass名の.is-hiddenclassList.toggleで付け替える処理を行います。

// スクロール量 
let lastScrollY = window.scrollY;
// スクロールイベントの連続発火を防ぐためのフラグ
let isTicking = false;

// 追従ナビゲーションの表示/非表示を切り替える関数
const updateNavVisibility = () => {
  const footerTop = footer.getBoundingClientRect().top;
  const firstSectionTop = firstSection.getBoundingClientRect().top;
  const isInview = footerTop <= 500 || firstSectionTop > 0;
  pageNav.classList.toggle('is-hidden', isInview);
}

const onScroll = () => {
  if (isTicking) return;
  lastScrollY = window.scrollY;
  isTicking = true;

  // 1フレーム待ってからinviewAnimationを実行
  requestAnimationFrame(() => {
    updateNavVisibility();
    updateCurrentSection();
    isTicking = false;
  });
}

// スクロールイベントの登録
window.addEventListener('scroll', onScroll);

これで、scrollに応じて特定のトリガー要素が画面上に表示された場合に、追従ナビゲーションの表示/非表示を切り替えることができます。

手順5(JavaScript / 表示位置に合わせたハイライトを追加)

scrollイベントの関数内で、全てのaタグから、class名is-currentを削除する記述と、ハイライト表示のトリガーとなるsection要素を取得します。

const updateCurrentSection = () => {
  navLinks.forEach((link) => {
    link.classList.remove('is-current')
    const sectionId = link.getAttribute('href')
    const section = document.querySelector(sectionId)
  })
}

次に、scrollイベント内の関数に、画面に表示されているsectionを判定する処理を記述します。
こちらでは、追従ナビゲーションのaタグのhref属性、sectionタグのid属性をローカル変数として宣言します。
現在のスクロール位置がそのsection内にある場合、if文を使って対応するaタグにclass名is-currentを追加してハイライトを適用します。

const updateCurrentSection = () => {
  navLinks.forEach((link) => {
    link.classList.remove('is-current')
    const sectionId = link.getAttribute('href')
    const section = document.querySelector(sectionId)
    if (section) {
      const sectionTop = section.offsetTop - 240
      const sectionBottom = sectionTop + section.offsetHeight
      if (lastScrollY >= sectionTop && lastScrollY <= sectionBottom) {
        link.classList.add('is-current')
      }
    }
  })
}
updateCurrentSection()

追従ナビゲーションを隠す処理と、現在位置のハイライトの付け替えを行う処理をまとめたJavaScriptは下記になります。

document.addEventListener("DOMContentLoaded", () => {
  const pageNav = document.getElementById('page-nav');
  const footer = document.getElementById('footer');
  const firstSection = document.getElementById('about');
  const navLinks = document.querySelectorAll(".page-nav__link");

  if (!pageNav || !footer || !firstSection || navLinks.length === 0) return;

  let lastScrollY = window.scrollY;
  let isTicking = false;

  const updateNavVisibility = () => {
    const footerTop = footer.getBoundingClientRect().top;
    const firstSectionTop = firstSection.getBoundingClientRect().top;
    const isInview = footerTop <= 500 || firstSectionTop > 0;
    pageNav.classList.toggle('is-hidden', isInview);
  }

  const updateCurrentSection = () => {
    navLinks.forEach(link => {
      link.classList.remove("is-current");
      const sectionId = link.getAttribute("href");
      const section = document.querySelector(sectionId);
      if (section) {
        const sectionTop = section.offsetTop - 240;
        const sectionBottom = sectionTop + section.offsetHeight;
        if (lastScrollY >= sectionTop && lastScrollY <= sectionBottom) {
          link.classList.add("is-current");
        }
      }
    });
  }

  const onScroll = () => {
    if (isTicking) return;
    lastScrollY = window.scrollY;
    isTicking = true;

    requestAnimationFrame(() => {
      updateNavVisibility();
      updateCurrentSection();
      isTicking = false;
    });
  }
  window.addEventListener('scroll', onScroll);

  updateNavVisibility();
  updateCurrentSection();
});

これで、scrollイベントを使用した、追従ナビゲーションの完成です!

実装手順 JavaScript (Intersection Observer編)

手順4(JavaScript / 追従ナビゲーションを隠す処理)

続きまして、Intersection Observerを使った実装の解説を行います。
(ユーザーがスクロールを行うたびに発生するscrollイベントに対して、要素を監視し、要素が画面に入ったことで実行されるIntersection Observerの方がパフォーマンスが高く、現在主流とされています。)

Intersection Observerも同様に、ナビゲーションと、追従ナビゲーションを隠すトリガー(今回はフッター)と、上方向の基準地点(今回は#about)変数を用意します。

Intersection Observerのopsionを、optionsという変数をオブジェクトで用意します。今回は初期値をそのまま引用しました。
それぞれのoptionの中身としては、

  • root: ビューポートを基準にするためnullを設定。
  • rootMargin: 追加のマージンを設定しないため0pxを設定。
  • threshold: 要素が10%表示されたときにコールバックを呼び出すため0.1を設定。

といった設定基準となります。

// Intersection Observerの option
const options = { root: null, rootMargin: '0px', threshold: 0.1 };

toggleNavVisibility関数で、entries(監視対象の要素に関する情報の配列)を受け取り、if文で先ほど宣言した変数と合致しているかを判別します。
IntersectionObserverに上記関数とオプションを登録し、FooterFirstSectionの監視を実行します。

const toggleNavVisibility = (entries) => {
  entries.forEach((entry) => {
    if (entry.target === Footer || entry.target === FirstSection) {
      pageNav.classList.toggle('is-hidden', entry.isIntersecting)
    }
  })
}

const navObserver = new IntersectionObserver(toggleNavVisibility, options)
navObserver.observe(Footer)
navObserver.observe(FirstSection)

手順5(JavaScript / 表示位置に合わせたハイライトを追加)

document.querySelectorを使用して、現在のentry.target(監視対象のセクション)のid属性に対応するナビゲーションリンクを取得します。
また、テンプレートリテラル(`)を使用して、href属性のセレクタ文字列を生成しています。

const updateLinkState = (entries) => {
  entries.forEach((entry) => {
    const link = document.querySelector(`.page-nav__link[href="#${entry.target.id}"]`)
    if (link) {
      if (entry.isIntersecting) {
        link.classList.add('is-current')
      } else {
        link.classList.remove('is-current', 'is-hidden')
      }
    }
  })
}

const linkObserver = new IntersectionObserver(updateLinkState, options)
document.querySelectorAll('section').forEach((section) => linkObserver.observe(section))

これで、Intersection Observer でもハイライト付きナビゲーションを実装することができました!

現在表示されているsection要素に応じて、対応する追従ナビゲーションのリンクにハイライトが適用されているはずです。
追従ナビゲーションの表示/非表示の判定範囲や、.is-currentが付与されたときのハイライトは、デザインに合わせてご自身でご調整ください!

参考文献

JavaScriptのIntersection Observerでスクロールに合わせてグラデーションの色を変更する

【jQuery】追従ナビゲーションのカレント表示