こんにちは、かつコーチです。
前回は、アコーディオンをjQueryで実装しながら、素のJavaScript版との書き方の違いを比較しました。
今回は、同じく「複数の要素からアクティブなものを1つだけ選ぶ」というUIである、タブ切り替えを実装していきます。
前回のアコーディオンで扱った「単独展開」の考え方が、そのままタブ切り替えの土台になります。
基本の実装手順
手順1:HTMLでタブとコンテンツを用意する
タブのボタンと、対応する中身をそれぞれ用意します。
<div class="tabs">
<ul class="tabs__nav">
<li class="tabs__nav-item is-active" data-target="tab1">料金プラン</li>
<li class="tabs__nav-item" data-target="tab2">機能一覧</li>
<li class="tabs__nav-item" data-target="tab3">よくある質問</li>
</ul>
<div class="tabs__content is-active" id="tab1">
<p>料金プランについての説明です。</p>
</div>
<div class="tabs__content" id="tab2">
<p>機能一覧についての説明です。</p>
</div>
<div class="tabs__content" id="tab3">
<p>よくある質問についての説明です。</p>
</div>
</div>
タブのボタン側にdata-target属性を持たせておき、対応するコンテンツのidと一致させておくのがポイントです。
data属性(data-から始まるカスタム属性で、要素に任意の情報を持たせられる仕組み)を使うことで、「どのタブがどのコンテンツに対応するか」をHTML側で明示できます。
手順2:CSSで表示・非表示を切り替える
.tabs__nav {
display: flex;
list-style: none;
padding: 0;
margin: 0;
border-bottom: 2px solid #ddd;
}
.tabs__nav-item {
padding: 12px 24px;
cursor: pointer;
color: #666;
}
.tabs__nav-item.is-active {
color: #333;
font-weight: bold;
border-bottom: 2px solid #3b82f6;
}
.tabs__content {
display: none;
padding: 16px;
}
.tabs__content.is-active {
display: block;
}
is-activeクラスが付いているタブ・コンテンツだけを目立たせる、というシンプルな設計です。
手順3:jQueryで切り替え処理を書く
$('.tabs__nav-item').on('click', function () {
const target = $(this).data('target');
// クリックされたタブをアクティブにし、他は解除する
$(this).addClass('is-active').siblings().removeClass('is-active');
// 対応するコンテンツだけを表示する
$('.tabs__content').removeClass('is-active');
$('#' + target).addClass('is-active');
});
ここで登場するのがsiblings()(同じ親を持つ兄弟要素をすべて取得するメソッド)です。
「クリックされた自分にクラスを付ける」→「兄弟要素からは全部クラスを外す」という2段構えにすることで、常に1つのタブだけがアクティブな状態を保てます。
$(this).data('target')で、HTMLに書いたdata-target属性の値をそのまま取得できる点も便利です。
siblings()を使ったアクティブ状態の管理
なぜ全要素を対象にしないのか
$('.tabs__nav-item').removeClass('is-active')のように、クラスを持つ全要素を対象にする書き方でも動きます。
$('.tabs__nav-item').on('click', function () {
$('.tabs__nav-item').removeClass('is-active'); // 全タブから一旦外す
$(this).addClass('is-active'); // 自分に付け直す
});
これでも正しく動作しますが、siblings()を使う書き方には次のようなメリットがあります。
- クリックされた
thisを起点にするため、「今どの要素を操作しているか」が読みやすい - タブグループが画面内に複数ある場合でも、同じグループ内だけを正しく対象にできる
特に2つ目は重要で、$('.tabs__nav-item').removeClass('is-active')という書き方では、ページ内に複数のタブグループがあると、無関係な別グループのタブまで解除されてしまいます。
siblings()は「クリックされた要素と同じ親を持つ兄弟要素」だけを対象にするため、タブグループごとに独立して動作させたいときに適した書き方です。
つまずきやすいポイント:タブは切り替わるのにコンテンツが変わらない
私が実際にハマった経験
私が最初にタブ切り替えを実装したとき、タブの見た目(is-activeの色)は正しく切り替わるのに、下のコンテンツだけまったく変わらない現象にハマりました。
コンソールにエラーは出ておらず、しばらくsiblings()周りを疑って時間を使いましたが、原因はコンテンツ側のIDセレクタの組み立て方にありました。
❌ Before:文字列結合を忘れてIDセレクタが壊れている
$('.tabs__nav-item').on('click', function () {
const target = $(this).data('target');
$('.tabs__content').removeClass('is-active');
$(target).addClass('is-active');
// targetは"tab1"という文字列そのもの
// $('tab1')は「tab1というタグ名の要素」を探しにいってしまい何も見つからない
});
data('target')で取得できるのは"tab1"という文字列そのものであり、"#tab1"というセレクタ形式ではありません。
そのまま$(target)と書いてしまうと、jQueryはtab1というタグ名の要素を探しにいってしまい、該当する要素が存在しないため何も起こらない、という結果になります。
✅ After:IDセレクタとして文字列を組み立てる
$('.tabs__nav-item').on('click', function () {
const target = $(this).data('target');
$('.tabs__content').removeClass('is-active');
$('#' + target).addClass('is-active');
// "#" + "tab1" で "#tab1" というIDセレクタになる
});
'#' + targetとすることで、"#tab1"という正しいIDセレクタの文字列を組み立てられます。
data()で取得した値をそのままセレクタとして使うのはよくあるミスなので、「data属性の値は生の文字列であり、セレクタには変換が必要」と覚えておくと防げます。
応用・一歩先の使い方
URLのハッシュと連動させる
タブの状態をURLの#tab2のようなハッシュ値と連動させると、リンクを共有したときに特定のタブを開いた状態で表示できます。
// 初期表示時にハッシュがあれば該当タブを開く
const hash = window.location.hash.replace('#', '');
if (hash) {
$('.tabs__nav-item[data-target="' + hash + '"]').trigger('click');
}
// タブ切り替え時にハッシュも更新する
$('.tabs__nav-item').on('click', function () {
const target = $(this).data('target');
window.history.replaceState(null, '', '#' + target);
});
trigger('click')を使うと、クリックイベントをJavaScript側から擬似的に発生させられます。
ページ読み込み時の初期化処理と、実際のクリック処理を1本化できるため、コードの重複を避けられます。
キーボード操作に対応させる
アクセシビリティを意識するなら、矢印キーでのタブ移動にも対応させると親切です。
$('.tabs__nav-item').on('keydown', function (e) {
if (e.key === 'ArrowRight') {
$(this).next('.tabs__nav-item').trigger('click').trigger('focus');
} else if (e.key === 'ArrowLeft') {
$(this).prev('.tabs__nav-item').trigger('click').trigger('focus');
}
});
next()・prev()とsiblings()はどちらも「兄弟要素を扱う」という点で共通しており、セットで覚えておくと応用が利きます。
まとめ
この記事のポイント
- タブ切り替えは、
data-target属性でタブとコンテンツを紐づけ、siblings()で兄弟要素の状態を管理するのが基本形 siblings()を使うと、複数のタブグループが同じページにあっても、それぞれ独立して切り替えられるdata()で取得した値は生の文字列なので、IDセレクタとして使う際は'#' + targetのように組み立てる必要があるtrigger('click')を使うと、初期表示処理とクリック処理を1つの関数にまとめられる
次に読むべき記事
前回のアコーディオン実装(jquery-accordion-hikaku)でも触れた「単独展開」の考え方は、今回のタブ切り替えとも共通しています。
あわせて読むことで、「複数要素から1つだけをアクティブにする」というUIパターンの理解がより深まります。