「PCではタブで切り替えたい、でもSPではページ内リンクで飛ばしたい」——レスポンシブサイトでよくある要件です。

この記事では、そのハイブリッドなタブUI(invisible-tabPanel_pc.js)の仕組みと使い方を、GSAPアニメーションとJavaScriptで実装したコードを交えて解説します。あわせて、SP/PC両方でタブ切り替えを行うバリエーション(invisible-tabPanel.js)との違いも紹介します。

HTML構成

タブパネル(コンテンツ側)

<div id="ch-Equipment-1" class="tabpanel">
  <!-- パネル1のコンテンツ -->
</div>

<div id="ch-Equipment-2" class="tabpanel">
  <!-- パネル2のコンテンツ -->
</div>

<div id="ch-Equipment-3" class="tabpanel">
  <!-- パネル3のコンテンツ -->
</div>

タブメニュー(ナビゲーション側)

<ul>
  <li class="tabs-item">
    <a class="tabs-link" href="/equipments/#ch-Equipment-1">KITCHEN</a>
  </li>
  <li class="tabs-item">
    <a class="tabs-link" href="/equipments/#ch-Equipment-2">BATHROOM</a>
  </li>
  <li class="tabs-item">
    <a class="tabs-link" href="/equipments/#ch-Equipment-3">ZEH</a>
  </li>
</ul>

ポイント: タブリンクの href#ch-Equipment-1(ハッシュのみ)でも https://example.com/equipments/#ch-Equipment-1(絶対URL)でも動作します。ただし、PHP側やCMSでデフォルトの current_page_item クラスが付与されている場合は、JS側でリセットするのでPHPテンプレートに current_page_item をハードコードしないことが重要です。

ホワイトアウト用オーバーレイ

<div class="overload"></div>

CSS

ホワイトアウト用オーバーレイ

/* .overload(遷移ホワイトアウト用) */
.overload {
  position: fixed;
  z-index: 20000;
  inset: 0;
  margin: auto;
  display: block;
  width: 100%;
  height: 100vh;
  background: #FFF;
  pointer-events: none;
  opacity: 0;
}

pointer-events: none にすることで、オーバーレイが表示中でもクリック操作がブロックされません。opacity: 0 が初期値で、GSAPで opacity: 10 とアニメーションさせます。


JavaScript(invisible-tabPanel_pc.js

3つのIIFEで構成されています。

パート1:CSSの動的挿入

(function () {
  const STYLE = document.createElement('style');
  STYLE.textContent = `
@media print, screen and (min-width: 744px) {
  .tabpanel {
    overflow: hidden;
    visibility: hidden;
    height: 0;
    opacity: 0;
  }
  .tabpanel.is-summoned {
    visibility: visible;
    height: auto;
    opacity: 1;
  }
}
`;
  document.getElementsByTagName('head')[0].appendChild(STYLE);
})();

解説:

  • @media screen and (min-width: 744px) により、PCのみ .tabpanel を非表示にします
  • SPでは通常の表示のままなので、タブリンクは普通のアンカーリンクとして機能します
  • .is-summoned クラスが付いたパネルだけが表示されます
  • @media print を含めることで、印刷時も全パネルが展開されます

パート2:ロード時の初期表示処理

(function () {
  let tabItems = Array.prototype.slice.call(document.querySelectorAll(".tabs-item"));
  let firstTabItem = document.querySelector('.tabs-item');
  let tabPanels = Array.prototype.slice.call(document.querySelectorAll(".tabpanel"));
  let firstTabPanel = document.querySelector('.tabpanel');

  const summonHash = window.location.hash.slice(1); // URLのハッシュ取得

  let timerBVJQ = 0;
  let fnAddSummonBVJQ = function () {
    if (timerBVJQ) clearTimeout(timerBVJQ);
    timerBVJQ = setTimeout(() => {

      if (!summonHash) {
        // ハッシュなし → 最初のパネルとタブをアクティブに
        if (firstTabPanel) firstTabPanel.classList.add('is-summoned');
        if (firstTabItem) firstTabItem.classList.add('current_page_item');

      } else {
        // ハッシュあり → 対応パネルをアクティブに
        const smnTabPanel = document.getElementById(summonHash);
        if (smnTabPanel) {
          // 全パネルのis-summonedをリセットしてから付与
          tabPanels.forEach(tp => tp.classList.remove('is-summoned'));
          smnTabPanel.classList.add('is-summoned');
        } else {
          // 対応パネルなし → フォールバックで最初のパネル
          if (firstTabPanel) firstTabPanel.classList.add('is-summoned');
        }

        // 対応タブアイテムをアクティブに($= 末尾一致で完全一致を実現)
        let smnTabItems = Array.prototype.slice.call(
          document.querySelectorAll(`.tabs-item>a[href$="#${summonHash}"]`)
        );
        if (smnTabItems.length > 0) {
          // 全タブのcurrent_page_itemをリセットしてから付与
          tabItems.forEach(ti => ti.classList.remove('current_page_item'));
          smnTabItems.forEach(a => a.parentElement.classList.add('current_page_item'));
        } else {
          // 対応タブなし → フォールバックで最初のタブ
          if (firstTabItem) firstTabItem.classList.add('current_page_item');
        }
      }

    }, 300); // 少し遅延させてDOMの準備を待つ
  };

  window.addEventListener('load', fnAddSummonBVJQ);
})();

解説:

  • window.location.hash.slice(1) でURLの #ch-Equipment-2ch-Equipment-2 を取得します
  • ハッシュなし → 最初のパネル・タブをアクティブにします(通常アクセス)
  • ハッシュあり → getElementById で対象パネルを特定します(直リンク・他ページからの遷移に対応)
  • タブセレクタは href$="#hash"末尾一致を使用。*= 部分一致にすると #ch-Equipment-3#ch-Equipment-32 の両方がヒットしてしまうため、末尾一致が正解です
  • setTimeout(..., 300) の遅延は、WordPressプラグイン等でDOMの生成が遅れる場合への対策です

パート3:タブクリック時の切り替え処理

(function () {
  let overLoad = document.querySelector(".overload");
  let tabItems = Array.prototype.slice.call(document.querySelectorAll(".tabs-item"));
  let tabLinks = Array.prototype.slice.call(document.querySelectorAll(".tabs-item>a"));
  let tabPanels = Array.prototype.slice.call(document.querySelectorAll(".tabpanel"));

  function fnClckTabBVJQ(tabLink) {
    tabLink.addEventListener('click', function (e) {
      // PCのみデフォルトイベント(ページ遷移)を無効化
      if (window.matchMedia('(min-width: 744px)').matches) e.preventDefault();

      const tabHref = tabLink.getAttribute('href');
      let targetHash, targetSelector;

      try {
        if (tabHref.startsWith('#')) {
          // #hash 形式
          targetHash = tabHref;
          targetSelector = tabHref;
        } else {
          // 絶対URL・相対URLからハッシュ部分のみ抽出
          const url = new URL(tabHref, window.location.origin);
          targetHash = url.hash;
          targetSelector = url.hash;
        }

        let targetTabPanel = document.querySelector(targetSelector);
        let sameTabItems = Array.prototype.slice.call(
          document.querySelectorAll(`.tabs-item>a[href*='${targetSelector}']`)
        ).map(a => a.parentElement);

        if (targetTabPanel) {
          history.pushState(null, '', targetHash); // URLのハッシュを更新

          // GSAPタイムラインで切り替えアニメーション
          let timeline = gsap.timeline();
          timeline
            .add(() => {
              gsap.to(overLoad, { duration: 0.3, opacity: 1 }); // オーバーレイをフェードイン
              gsap.to(targetTabPanel, { duration: 0, opacity: 0 });
              if (window.matchMedia('(min-width: 744px)').matches) {
                gsap.to('body', { onComplete: () => window.scrollTo(0, 0) }); // ページトップへ
              }
            })
            .to(targetTabPanel, {
              onComplete: function () {
                // 全パネル・全タブのクラスをリセット
                tabPanels.forEach(tp => tp.classList.remove('is-summoned'));
                tabItems.forEach(ti => ti.classList.remove('current_page_item'));
              }
            })
            .add(() => {
              targetTabPanel.classList.add('is-summoned'); // 対象パネルを表示
              sameTabItems.forEach(ti => ti.classList.add('current_page_item')); // 対象タブをアクティブに
            })
            .to(targetTabPanel, { opacity: 1 })
            .to(overLoad, { duration: 0.15, opacity: 0 }); // オーバーレイをフェードアウト

          timeline.play();
        }

      } catch (error) {
        console.error('エラーが発生しました:', error);
      }
    });
  }

  tabLinks.forEach(tabLink => fnClckTabBVJQ(tabLink));
})();

解説:

  • window.matchMedia('(min-width: 744px)').matches でPCかどうかを判定し、PCのみ e.preventDefault() でページ遷移をキャンセルします
  • SPではデフォルトのアンカーリンク動作(ページ内スクロール)が有効のままです
  • タブリンクの href が絶対URLでも new URL() でハッシュ部分のみを抽出します
  • GSAPタイムラインの流れ:
    1. オーバーレイをフェードイン(白くする)
    2. ページトップへスクロール(PC)
    3. 全クラスをリセット
    4. 対象パネル・タブをアクティブ化
    5. オーバーレイをフェードアウト

使い方パターン

パターンA:PCのみタブ切り替え、SPはアンカーリンク(基本)

invisible-tabPanel_pc.js を使用

<script src="[topthemeurl]/js/_gimmicks/invisible-tabPanel_pc.js"></script>

パターンB:SP/PC両方でタブ切り替え

invisible-tabPanel.js を使用

<script src="[topthemeurl]/js/_gimmicks/invisible-tabPanel.js"></script>

invisible-tabPanel.js との主な違いは以下の2点です。

項目 invisible-tabPanel_pc.js invisible-tabPanel.js
CSSのメディアクエリ min-width: 744px(PCのみ非表示) なし(全幅で非表示)
クリック時のpreventDefault PCのみ SP/PC両方

パターンC:別ページのタブパネルへ直リンク

<!-- 別ページのリンク(SPでは通常リンク、PCではパネル直接表示) -->
<a href="https://example.com/equipments/#ch-Equipment-3">ZEHについて</a>

URLにハッシュが含まれていれば、fnAddSummonBVJQ が自動的に対応パネルを表示します。追加実装不要です。


よくあるトラブルと対処

タブが2つ同時にアクティブになる

CMSやWordPressのナビゲーション機能が current_page_item をHTMLにハードコードしている場合に起こります。PHPテンプレートの current_page_item をあらかじめ削除しておくか、JS側のリセット処理(classList.remove('current_page_item'))が正しく走るよう確認してください。

他ページから遷移してきたときに最初のパネルが表示される

smnTabItems のセレクタが href$=(末尾一致)になっているか確認します。href*=(部分一致)にすると #id-3#id-32 の両方がヒットするため、末尾一致が正解です。

タブパネルが切り替わらない(SPで e.preventDefault() が効いていない)

invisible-tabPanel.js(SP/PC両対応)を使っているのに、SP側で e.preventDefault() がコメントアウトされていないか確認してください。


まとめ

このシステムの優れている点は、PCではSPAライクなタブUI、SPでは標準的なアンカーリンクを1つのコードで実現しているところです。URLのハッシュを軸にした設計なので、他ページからの直リンクやブラウザの戻る/進むとも自然に連携します。

用途に応じて invisible-tabPanel_pc.js(PCのみタブ)と invisible-tabPanel.js(全デバイスでタブ)を使い分けてください。