「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: 1 → 0 とアニメーションさせます。
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-2→ch-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タイムラインの流れ:
- オーバーレイをフェードイン(白くする)
- ページトップへスクロール(PC)
- 全クラスをリセット
- 対象パネル・タブをアクティブ化
- オーバーレイをフェードアウト
使い方パターン
パターン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(全デバイスでタブ)を使い分けてください。
