jsでもっとちゃんとタブUIを実装する(jQuery不使用)- ページ更新してもタブがリセットされないやり方

※本ページはプロモーションを含みます

WEBサイトに一般的なタブのUIを追加したい時、インターネットで実装のサンプルコードを探すことがあると思います。
たくさんの記事が検索でひっかかりますが「とりあえず動けば良い」というようなコードも少なくありません。

・タブを押せて該当するコンテンツが表示される動きはできているがブラウザを更新したらタブが戻る
・タブのコンテンツ内のアンカーリンクをクリックしたらタブの動きがおかしくなる
・jQueryを読み込まないと動かない

このような不具合に出会ったことはないでしょうか?
この記事では上記のような不具合が発生しないjQueryに依存しないタブUIの作成方法を紹介します。

結果のコード

See the Pen perfect-tab-unit by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

<div class="tab-container">
  <nav class="tabs">
    <a href="#tab1" class="tab">tab1</a>
    <a href="#tab2" class="tab">tab2</a>
    <a href="#tab3" class="tab">tab3</a>
  </nav>
  <div id="tab1" class="tab-cont">
    <h2>tab1-title</h2>
    <div>
      tab1のコンテンツ
    </div>
  </div>
  <div id="tab2" class="tab-cont">
    <h2>tab2-title</h2>
    <div>
      tab2のコンテンツ
    </div>
  </div>
  <div id="tab3" class="tab-cont">
    <h2>tab3-title</h2>
    <div>
      tab3のコンテンツ
    </div>
  </div>
</div>
.tab-container {
  --border-color: #ccc;
  --active-color: beige;
  --inactive-color: lightgray;
  --container-width: 500px;
  
  max-width: var(--container-width);
  margin-right: auto;
  margin-left: auto;
  padding: 30px;
  
  .tabs {
    display: flex;
    column-gap: 4px;
    position: relative;
    bottom: -1px;
    left: 15px;
  }

  .tab {
    border: solid 1px var(--border-color);
    padding: 10px 15px;
    line-height: 1;
    border-radius: 6px 6px 0 0;
    background-color: var(--inactive-color);

    &.active {
      border-bottom-color: var(--active-color);
      background-color: var(--active-color);
    }
  }

  .tab-cont {
    border: solid 1px var(--border-color);
    padding: 30px;
    border-radius: 6px;

    &.hidden {
      display: none; 
    }

    &.active {
      display: block; 
      background-color: var(--active-color);
    }
  }
}

CSSはネスト方式で書いています。また、CSS変数を利用していますが後で変更処理をしなければいけなくなったときに効率にかなり差がでますので積極的に使用したいところです。

※ネスト方式の各ブラウザ対応はこちらで確認できます。
https://caniuse.com/?search=nesting

(function tabUnit(){
  
  const tabs = document.querySelectorAll('.tab');
  const tabConts = document.querySelectorAll('.tab-cont');
  const tabUnit = document.querySelector('.tab-container');
  
  tabUnit.addEventListener('selectTab', (e) => {
    resetTabs();
    setTab(e.detail);
  }, false);
  
  function resetTabs() {
    tabConts.forEach((tabCont) => {
      tabCont.classList.remove('active');
      tabCont.classList.add('hidden');
    });
    tabs.forEach((tab) => {
      tab.classList.remove('active');
    });
  }
  
  function setTab(hashString) {
    Array.from(tabs).some((tab) => {
      if (tab.getAttribute('href') === hashString) {
        tab.classList.add('active');
        return true;
      }
    });
    
    Array.from(tabConts).some((tabCont) => {
      if (tabCont.getAttribute('id') === hashString.slice(1)) {
        tabCont.classList.add('active');
        return true;
      }
    });
  }
  
  document.addEventListener("DOMContentLoaded",() => {
    const hash = location.hash;
    let nextHash = '';
    if (hash === '') {
      nextHash = document.querySelector('.tab').getAttribute('href');
    } else {
      nextHash = hash;
    }
    const evt = new CustomEvent('selectTab', {detail: nextHash});
    tabUnit.dispatchEvent(evt);
  });

  tabs.forEach((tab) => {
    tab.addEventListener("click",(e) => {
      e.preventDefault();
      const nextHash = e.currentTarget.getAttribute('href');
      history.pushState('', '', nextHash);
      const evt = new CustomEvent('selectTab', {detail: nextHash});
      tabUnit.dispatchEvent(evt);
    });
  });

})();

処理の流れは今回は次のようにしました。

1.タブの状態をリセットする関数を作成する(resetTabs)
2.タブの状態をセットする関数を作成する(setTab)
3.このタブセット全体がカスタムイベントを受けて行う処理を設定する
4.ページが読み込まれたときにカスタムイベントを生成し発火する
5.タブがクリックされたときにカスタムイベントを生成し発火する

これでURLにハッシュ(♯から始まる文字列)がない場合はタブの一番名のコンテンツが有効化されタブをクリックすると連動したコンテンツが有効化、さらにページを更新してもURLに付与されたハッシュを認識して対応する箇所が有効化されるタブができました。

カスタムイベントのおさらい

▼イベントの作成と起動 – イベントリファレンス – MDN Web Docs
https://developer.mozilla.org/ja/docs/Web/Events/Creating_and_triggering_events

ポイント

querySelectorAll()で取得した要素のループ処理でのbreak

document.querySelectorAll()で取得した返り値は<NodeList>というタイプで配列ではありません。

戻り値の要素でforEach構文を利用して下記のように書きたいところですがこれは意図したとおりに動きません。

// NG
const tabs = document.querySelectorAll('.tab');
tabs.forEach((tab, index) => {
  if(index === 1) {
    // tabを有効化する処理
    break; // 一つだけ処理できればその後は不要なので抜け出したい。でもNG
  }
});

forEach構文ではbreakが効きませんのでbreakのようにループを抜けるようにするためにはfor文かsome関数を使う必要があります。

for文でもよいんですが今回はsome関数を利用します。しかし、some関数は配列じゃないとだめなので少し工夫して下記のようにします。

const tabs = document.querySelectorAll('.tab');
Array.from(tab).some((tab, index) => {
  if(index === 1) {
    // tabを有効化する処理
    return true // trueを返せばループを抜ける
  }
});

これで該当のタブがみつかったら有効化処理をしてすぐループを抜ける(そのあとの処理は行わない)動きを実現できます。

some()関数のおさらい

▼Array.prototype.some() – JavaScript – MDN Web Docs
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/some

hashchangeイベントでタブ切り替えを行わない

hashchangeイベントで切り替えるとタブコンテンツ内にアンカーリンクがある場合に処理が混乱しやすくなるので使用は控えてhashchangeイベントを発火しないhistory.pushState()でURLにハッシュを追加します。

history.pushState()のおさらい

▼History: pushState() メソッド – Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/History/pushState

最後に

ネットの記事はさっくり知識を得るには便利ですが本番レベルなのか判断するのは初心者には難しいかもしれません。

自分で実装することで後の修正や改造が楽になる場合があります。

もっと知識を深めたい場合にはスクールや本で勉強しましょう。