1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モダンJSで作るタブ

1
Last updated at Posted at 2025-08-11

初心者向け:モダン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-controlsidの対応関係

<!-- この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. タブがクリックされたら

  1. 全部リセット:すべてのタブを非アクティブ、すべてのパネルを非表示
  2. クリックしたタブをアクティブaria-selected="true"にする
  3. 対応するパネルを表示hiddenを外す

❗ よくあるトラブルと解決法

トラブル1:「タブをクリックしても何も起こらない」

原因:aria-controlsidが一致していない

<!-- ❌ 間違い -->
<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')

🚀 さらなる改善のアイデア

  1. キーボード操作対応:矢印キーでタブを切り替える
  2. アニメーション:パネル切り替え時にフェード効果
  3. URL連携:ブラウザの戻る/進むでタブ状態も変わる

📝 まとめ

  • 対応付けが重要aria-controlsidを必ず一致させる
  • 安全な実装:要素が存在するかチェックしてからアクセス
  • スコープを意識:各タブグループ内だけで処理を完結させる
  • アクセシビリティrole属性で支援技術にも対応

これで安全で使いやすいタブUIが作れます!コピー&ペーストで試してみてくださいね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?