タブとは

見出し(タブ)を選ぶと、その下の内容だけが切り替わって表示される部品。

概要

複数の見出し(タブ)を横に並べておき、クリックすると選んだタブに対応する内容だけが表示されるパーツです。 同時に表示されるのは 1 つの内容だけなので、関連はあるけれど同時には見せなくてよい情報をコンパクトに まとめたいときに使われます。商品説明ページの「詳細・レビュー・送料」の切り替えなどが代表例です。

別名

表記ゆれ:タブ、タブ切り替え、タブメニュー、tabs

AI への伝え方

「タブを実装して」で通じます。アクセシビリティ対応まで含めたい場合は「WAI-ARIA のタブパターンで」と 伝えると、role="tablist" / role="tab" / role="tabpanel" と、左右の矢印キーでタブ移動できる実装まで 含めて作ってもらいやすくなります。非表示のパネルには hidden 属性を使うのが基本です。

似たパーツとの違い

似た部品に「アコーディオン」があります。アコーディオンは複数の項目を同時に開いたまま並べられるのに対し、 タブは常に 1 つの内容しか表示できない点が異なります。スマホ画面下部で切り替える「ボトムナビゲーション」も 仕組みは近いですが、ページそのものを切り替えるナビゲーションである点がタブと異なります。

デモ

商品の詳細情報がここに表示されます。素材やサイズなどの説明が入ります。

コード

HTML
<div class="tabs-demo">
  <div class="tabs-demo__list" role="tablist" aria-label="商品情報">
    <button
      class="tabs-demo__tab"
      type="button"
      role="tab"
      id="tabs-demo-tab-1"
      aria-selected="true"
      aria-controls="tabs-demo-panel-1"
      tabindex="0"
    >
      詳細
    </button>
    <button
      class="tabs-demo__tab"
      type="button"
      role="tab"
      id="tabs-demo-tab-2"
      aria-selected="false"
      aria-controls="tabs-demo-panel-2"
      tabindex="-1"
    >
      レビュー
    </button>
    <button
      class="tabs-demo__tab"
      type="button"
      role="tab"
      id="tabs-demo-tab-3"
      aria-selected="false"
      aria-controls="tabs-demo-panel-3"
      tabindex="-1"
    >
      送料
    </button>
  </div>

  <div class="tabs-demo__panel" id="tabs-demo-panel-1" role="tabpanel" aria-labelledby="tabs-demo-tab-1" tabindex="0">
    <p>商品の詳細情報がここに表示されます。素材やサイズなどの説明が入ります。</p>
  </div>
  <div
    class="tabs-demo__panel"
    id="tabs-demo-panel-2"
    role="tabpanel"
    aria-labelledby="tabs-demo-tab-2"
    tabindex="0"
    hidden
  >
    <p>レビューがここに表示されます。星評価やコメントの一覧が入ります。</p>
  </div>
  <div
    class="tabs-demo__panel"
    id="tabs-demo-panel-3"
    role="tabpanel"
    aria-labelledby="tabs-demo-tab-3"
    tabindex="0"
    hidden
  >
    <p>送料についての説明がここに表示されます。</p>
  </div>
</div>
CSS
.tabs-demo {
  width: 320px;
  font-family: sans-serif;
}

.tabs-demo__list {
  display: flex;
  border-bottom: 2px solid #e6e6e6;
}

.tabs-demo__tab {
  padding: 10px 16px;
  border: 0;
  background: transparent;
  color: #666;
  font-size: 14px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
}

.tabs-demo__tab:hover {
  color: #0017c1;
}

.tabs-demo__tab[aria-selected="true"] {
  color: #0017c1;
  font-weight: bold;
  border-bottom-color: #0017c1;
}

.tabs-demo__panel {
  padding: 16px 4px;
  font-size: 14px;
  color: #333;
  line-height: 1.7;
}

.tabs-demo__panel p {
  margin: 0;
}

/* hidden 属性を使う要素なので、display を指定するときは必ず [hidden] を打ち消す */
.tabs-demo__panel[hidden] {
  display: none;
}
JS
const tabs = Array.from(document.querySelectorAll(".tabs-demo__tab"));
const panels = Array.from(document.querySelectorAll(".tabs-demo__panel"));

function selectTab(newTab) {
  tabs.forEach((tab) => {
    const isSelected = tab === newTab;
    tab.setAttribute("aria-selected", String(isSelected));
    tab.tabIndex = isSelected ? 0 : -1;
  });
  panels.forEach((panel) => {
    panel.hidden = panel.getAttribute("aria-labelledby") !== newTab.id;
  });
  newTab.focus();
}

tabs.forEach((tab) => {
  tab.addEventListener("click", () => selectTab(tab));

  tab.addEventListener("keydown", (event) => {
    const currentIndex = tabs.indexOf(tab);
    let nextIndex = null;

    if (event.key === "ArrowRight") {
      nextIndex = (currentIndex + 1) % tabs.length;
    } else if (event.key === "ArrowLeft") {
      nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
    } else if (event.key === "Home") {
      nextIndex = 0;
    } else if (event.key === "End") {
      nextIndex = tabs.length - 1;
    }

    if (nextIndex !== null) {
      event.preventDefault();
      selectTab(tabs[nextIndex]);
    }
  });
});