スクロール位置でナビのカレント状態が変化するJS【ページ内リンク】

縦長のページでよくスクロールすると位置でナビゲーションの状態が変化するサイトをみたことがありませんか?
状態としてはページの現在地を知らせるためのUIが多いですね。

この記事ではそのJSの作り方をご紹介します!

スクロール位置でナビのカレント状態が変化するJS

デモ

まずはデモページを用意しましたので御覧ください。

デモページ:/demo/page-position-nav

作り方(実装)

それではスクロール位置でナビの状態が変化するJSの紹介です。

スクロール用ライブラリ「ScrollMagic」を読み込み

今回のJSにはScrollMagicというライブラリを使用します。
僕の記事ではよく登場するライブラリです。
個人的にはスクロール関連の処理はこれがあればある程度実装できてしまうと思っています。

ScrollMagic公式

以下、読み込み方法です。

CDN(scriptタグ)の場合

<script src="//cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>

NPMの場合

npm install scrollmagic
import ScrollMagic from "scrollmagic";

JavaScript

この記事のメインであるJavaScriptです。
以下の内容をコピペしてくだい。

※特に解説はしませんがコメントを記載しています

class PagePositionNav {
  constructor() {
    this.controller = new ScrollMagic.Controller();//ScrollMagicのコントローラー
    this.sections = "[data-page-section]";//セクションのdata属性
    this.settings = {
      addClassName: "is-current"//ナビに付与するclass
    };
  }

  init(options) {
    this.setup(options);
    this.attachEvent(this.controller);
  }

  //外部から入力された設定をマージ
  setup(options) {
    this.settings = Object.assign(
      {
        addClassName: this.settings.addClassName
      },
      options || {}
    );
  }

  //ScrollMagicを実行
  attachEvent(controller) {
    let sections = document.querySelectorAll(this.sections);

    for (let section of sections) {
      let scene_pagePositionNav = new ScrollMagic.Scene({
        triggerElement: section,
        triggerHook: "onCenter",
        duration: section.clientHeight,//実行範囲を拡張(セクションの高さいっぱいまで)
        offset: 0
      })
        // .addIndicators()
        .addTo(controller);

      //[data-page-section]エリアに入った場合
      scene_pagePositionNav.on("enter", () => {
        let targetElement = scene_pagePositionNav.triggerElement();//現在地のセクションのエレメントを取得
        this.removeNavActiveClass();//classをリセット これはなくてもいいかも
        document.querySelector(`[ data-page-nav = ${targetElement.getAttribute("data-page-section")}]`).classList.add(this.settings.addClassName);//セクションのデータ属性と同じ値を持つナビゲーションにclassを付与
      });

      //[data-page-section]エリアを抜けたとき
      scene_pagePositionNav.on("leave", () => {
        this.removeNavActiveClass();//classをリセット
      });

      //リサイズやスクロールでアップデート
      scene_pagePositionNav.on("update", function(event) {
        this.duration(section.clientHeight);//durationをアップデート
      });
    }
  }

  removeNavActiveClass() {
    let navElements = document.querySelectorAll("[data-page-nav]");
    for (let navElement of navElements) {
      navElement.classList.remove(this.settings.addClassName);
    }
  }

}

実行

const pagePositionNav = new PagePositionNav();
pagePositionNav.init()

HTML

デモページで使用しているHTMLです。

ここで重要なのはdata属性のdata-page-sectiondata-page-navです。
正直このdata属性があれば他は好きに書き換えて大丈夫です。
これに関しては使い方で解説します。

セクション部分

<section id="section1" class="demo-section -background-color-01" data-page-section="pos1">
  <h2>セクション1</h2>
</section>
<section id="section2" class="demo-section -background-color-02" data-page-section="pos2">
  <h2>セクション2</h2>
</section>
<section id="section3" class="demo-section -background-color-03" data-page-section="pos3">
  <h2>セクション3</h2>
</section>
<section id="section4" class="demo-section -background-color-04" data-page-section="pos4">
  <h2>セクション4</h2>
</section>

ナビゲーション部分

<div class="c-pagePositionNav">
  <ul class="c-pagePositionNav-list">
    <li class="c-pagePositionNav-list-item" data-page-nav="pos1">
      <a href="#section1" class="c-pagePositionNav-list-item__inner">
        <span class="c-pagePositionNav-list-item__text">セクション1</span>
      </a>
    </li>
    <li class="c-pagePositionNav-list-item" data-page-nav="pos2">
      <a href="#section2" class="c-pagePositionNav-list-item__inner">
        <span class="c-pagePositionNav-list-item__text">セクション2</span>
      </a>
    </li>
    <li class="c-pagePositionNav-list-item" data-page-nav="pos3">
      <a href="#section3" class="c-pagePositionNav-list-item__inner">
        <span class="c-pagePositionNav-list-item__text">セクション3</span>
      </a>
    </li>
    <li class="c-pagePositionNav-list-item" data-page-nav="pos4">
      <a href="#section4" class="c-pagePositionNav-list-item__inner">
        <span class="c-pagePositionNav-list-item__text">セクション4</span>
      </a>
    </li>
  </ul>
</div>

CSS(SCSS)

デモページで使用しているCSSです。

これは動作に影響するものではないので好きに書き換えて大丈夫です。

.demo-section {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 800px;
  margin: 0 calc((50% - 50vw));
  padding: 0 calc((50vw - 50%));
  background: #ccc;
  &.-background-color-01 {
    background: #3CB371;
  }
  &.-background-color-02 {
    background: #48D1CC ;
  }
  &.-background-color-03 {
    background: #CD5C5C;
  }
  &.-background-color-04 {
    background: #1E90FF;
  }
}
.c-pagePositionNav {
    position: fixed;
    right: 0;
  top: 50%;
    transform: translate(0, -50%);
  z-index: 1000;
  padding-right: 10px;
    transition: transform .5s cubic-bezier(0.39, 0.575, 0.565, 1);
}

.c-pagePositionNav-list-item {
    & + & {
        margin-top: 20px;
    }
}

.c-pagePositionNav-list-item__inner {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    position: relative;
    &:after {
        display: block;
        content: "";
        width: 4px;
        height: 4px;
        background: #333;
        margin-left: 10px;
    transition: width .5s;
    @at-root #{selector-replace(&, ".c-pagePositionNav-list-item__inner", ".c-pagePositionNav-list-item.is-current .c-pagePositionNav-list-item__inner")} {
      background: #fff;
            width: 50px;
    }
    }
    &:hover {
        text-decoration: none;
    }
}

.c-pagePositionNav-list-item__text {
  color: #333;
  font-size: 16px;
  @at-root #{selector-replace(&, ".c-pagePositionNav-list-item__text", ".c-pagePositionNav-list-item.is-current .c-pagePositionNav-list-item__text")} {
    color: #fff;
  }
}

使い方

ここからは使い方の紹介です。
基本的にはデータ属性をHTMLに付与するだけです。

HTMLにデータ属性を付与

セクション

セクション(コンテンツ毎のエリア)にデータ属性data-page-sectionを付与します。
以下ではsectionタグですがdivでもなんでも構いません。

<section data-page-section="任意の値01"></section>
<section data-page-section="任意の値02"></section>

ナビゲーション

ナビゲーションにデータ属性data-page-navを付与します。
セクションと同様ですがタグはul liでなくても構いません。

<ul>
  <li data-page-nav="任意の値01"></li>
  <li data-page-nav="任意の値02"></li>
</ul>

重要なのはセクションとナビゲーションをリンクされたいもののデータ属性の値を同じに設定します。

JSの実行方法

javascriptの紹介部分でも書きましたが実行方法は以下です。

const pagePositionNav = new PagePositionNav();
pagePositionNav.init()

実行されたときのHTMLの状態

JSが実行されていると
スクロール位置がdata-page-section="任意の値01"の位置だと
ナビゲーションのHTMLが以下のようになります。

<ul>
  <li class="is-current" data-page-nav="任意の値01"></li>
  <li data-page-nav="任意の値02"></li>
</ul>

上記の用にデータ属性の値が同じナビゲーションにis-currentclassが付与されます。
これを利用してCSSを記述して状態の変化をあらわします。

data-page-navが付与されているHTMLにclassが付与されます。

付与されるclassを変更

以下のように実行することで付与されるclassを変更できます。

pagePositionNav.init({
  addClassName: "is-active"
})

最後に

以上でスクロール位置でナビの状態が変化するJSの紹介は終わりです。
デモページにはスムーススクロールを実装していますがこのJSの機能ではありません。

smooth-scroll.jsというライブラリを使用しています。