初心者向け:モダンJavaScriptでタブUIを作ろう
🎯 この記事で学べること
- 複数のタブを同じページに置いても大丈夫な作り方
- よくあるエラー「Cannot set properties of null」を避ける方法
- アクセシビリティに配慮したタブの実装
🌟 完成イメージ
タブをクリックすると内容が切り替わる、よく見るあのUIです!
[タブ1] [タブ2] ← これをクリックすると
──────────────
パネル1の内容 ← この部分が切り替わる
📁 ファイル構成
プロジェクトフォルダ/
├─ index.html ← メインのHTMLファイル
├─ styles.css ← タブのデザイン
└─ src/
└─ main.js ← タブの動作を制御するJavaScript
🏗️ HTML:土台を作ろう
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="./styles.css" rel="stylesheet">
<title>タブUIサンプル</title>
</head>
<body>
<!-- 1つ目のタブグループ -->
<div class="tabs">
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="a-panel1" id="a-tab1">
タブ1
</button>
<button role="tab" aria-selected="false" aria-controls="a-panel2" id="a-tab2">
タブ2
</button>
</div>
<div id="a-panel1" role="tabpanel" aria-labelledby="a-tab1">
パネル1の内容です
</div>
<div id="a-panel2" role="tabpanel" aria-labelledby="a-tab2" hidden>
パネル2の内容です
</div>
</div>
<!-- 2つ目のタブグループ(同じページに複数置ける!) -->
<div class="tabs">
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="b-panel1" id="b-tab1">
タブ1
</button>
<button role="tab" aria-selected="false" aria-controls="b-panel2" id="b-tab2">
タブ2
</button>
</div>
<div id="b-panel1" role="tabpanel" aria-labelledby="b-tab1">
別のパネル1です
</div>
<div id="b-panel2" role="tabpanel" aria-labelledby="b-tab2" hidden>
別のパネル2です
</div>
</div>
<script src="./src/main.js"></script>
</body>
</html>
🔍 HTMLのポイント解説
1. role属性って何?
-
role="tablist":「ここはタブのグループですよ」という意味 -
role="tab":「これはタブボタンです」という意味 -
role="tabpanel":「これはタブの中身です」という意味
2. aria-controlsとidの対応関係
<!-- この2つを必ず対応させる! -->
<button aria-controls="a-panel1">タブ1</button> ← これが
<div id="a-panel1">パネル1の内容</div> ← これを制御する
3. 最初に表示したいタブは
- タブに
aria-selected="true"を付ける - パネルには
hiddenを付けない
⚡ JavaScript:動作を作ろう
// src/main.js
document.addEventListener("DOMContentLoaded", () => {
// すべての .tabs を見つける
document.querySelectorAll('.tabs').forEach(tabsContainer => {
// このタブグループ内のタブとパネルを取得
const tabs = tabsContainer.querySelectorAll('[role="tab"]');
const panels = tabsContainer.querySelectorAll('[role="tabpanel"]');
// 各タブにクリックイベントを設定
tabs.forEach(tab => {
tab.addEventListener("click", () => {
// ステップ1: すべてのタブを非アクティブにする
tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
panels.forEach(p => p.hidden = true);
// ステップ2: クリックされたタブをアクティブにする
tab.setAttribute('aria-selected', 'true');
// ステップ3: 対応するパネルを見つける(安全に!)
const targetId = tab.getAttribute('aria-controls');
// エラー防止:aria-controlsが設定されているかチェック
if (!targetId) {
console.warn('aria-controlsが設定されていないタブがあります:', tab);
return;
}
// IDに特殊文字が含まれていても安全に検索
const safeId = (window.CSS && CSS.escape) ? CSS.escape(targetId) : targetId;
const panel = tabsContainer.querySelector(`#${safeId}`);
// エラー防止:パネルが見つかるかチェック
if (!panel) {
console.warn('対応するパネルが見つかりません:', targetId);
return;
}
// ステップ4: 見つけたパネルを表示
panel.hidden = false;
});
});
});
});
🛡️ エラー防止のポイント
よくあるエラー:Cannot set properties of null
これは「存在しない要素に何かしようとした」時に起こります。
// ❌ 危険な書き方
const panel = document.querySelector('#存在しないID');
panel.hidden = false; // ← nullに対してhiddenを設定しようとしてエラー!
// ✅ 安全な書き方
const panel = document.querySelector('#存在しないID');
if (!panel) {
console.warn('パネルが見つかりません');
return; // ここで処理を止める
}
panel.hidden = false; // ← パネルが存在する時だけ実行
🎨 CSS:見た目を整えよう
/* styles.css */
/* タブボタンの並び */
[role="tablist"] {
display: flex;
gap: 4px;
margin-bottom: 0.5em;
border-bottom: 2px solid #ccc;
}
/* タブボタンの基本スタイル */
[role="tab"] {
padding: 0.5em 1em;
border: none;
background: #f8f8f8;
cursor: pointer;
font: inherit;
border-radius: 4px 4px 0 0;
transition: background 0.2s ease;
}
/* アクティブなタブ(選択中のタブ) */
[role="tab"][aria-selected="true"] {
background: #efefef;
border-bottom: 2px solid white;
font-weight: bold;
}
/* マウスを乗せた時 */
[role="tab"]:hover {
background: #e4e4e4;
}
/* キーボード操作時のフォーカス表示 */
[role="tab"]:focus {
outline: 2px solid #0078d7;
outline-offset: 2px;
}
/* パネル(タブの中身)のスタイル */
[role="tabpanel"] {
padding: 1em;
border: 1px solid #ccc;
border-radius: 0 4px 4px 4px;
background: white;
}
/* 隠れているパネル */
[role="tabpanel"][hidden] {
display: none;
}
/* タブグループ間の余白 */
.tabs:first-child {
margin-bottom: 2rem;
}
🔧 動作の仕組み
1. ページが読み込まれたら
document.addEventListener("DOMContentLoaded", () => {
// ページの読み込みが完了してから実行される
});
2. すべてのタブグループを見つける
document.querySelectorAll('.tabs').forEach(tabsContainer => {
// .tabsクラスがついた要素すべてに対して処理を実行
});
3. タブがクリックされたら
- 全部リセット:すべてのタブを非アクティブ、すべてのパネルを非表示
-
クリックしたタブをアクティブ:
aria-selected="true"にする -
対応するパネルを表示:
hiddenを外す
❗ よくあるトラブルと解決法
トラブル1:「タブをクリックしても何も起こらない」
原因:aria-controlsとidが一致していない
<!-- ❌ 間違い -->
<button aria-controls="panel-1">タブ</button>
<div id="panel1">内容</div> <!-- IDが違う! -->
<!-- ✅ 正解 -->
<button aria-controls="panel-1">タブ</button>
<div id="panel-1">内容</div>
トラブル2:「Cannot set properties of null」エラー
**原因:**存在しない要素を操作しようとしている
**解決:**JavaScriptで事前にチェックする(上記のコードに組み込み済み)
トラブル3:「複数のタブグループが干渉する」
**原因:**全体から要素を検索している
**解決:**各タブグループ内だけで検索する
// ❌ 全体から検索(他のタブグループも影響を受ける)
document.querySelector('#panel-1')
// ✅ タブグループ内だけで検索
tabsContainer.querySelector('#panel-1')
🚀 さらなる改善のアイデア
- キーボード操作対応:矢印キーでタブを切り替える
- アニメーション:パネル切り替え時にフェード効果
- URL連携:ブラウザの戻る/進むでタブ状態も変わる
📝 まとめ
-
対応付けが重要:
aria-controlsとidを必ず一致させる - 安全な実装:要素が存在するかチェックしてからアクセス
- スコープを意識:各タブグループ内だけで処理を完結させる
-
アクセシビリティ:
role属性で支援技術にも対応
これで安全で使いやすいタブUIが作れます!コピー&ペーストで試してみてくださいね。