tab-navigation について
タブの選択と実装を扱うデザインスキルです。レイアウト、オーバーフロー、アクセシビリティ、キーボード操作、状態の保存を解説し、関連ビューを設計する人を対象としています。
主な機能
- 01タブの適用場面に関する指針
- 02下線・ボックス・ピル型のスタイル
- 03オーバーフローと縦型タブのパターン
- 04キーボードと ARIA のアクセシビリティ
- 05URL と localStorage による状態保存
活用シーン
- 01関連コンテンツの表示にタブを選ぶ
- 02狭い画面でのタブ動作を設計する
- 03キーボードとスクリーンリーダー対応を確認する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dembrandt/ dembrandt-skills --skill tab-navigationインストール後、Claude Code で/tab-navigationを入力して読み込まれたか確認します。
詳細
タブナビゲーション
タブは、共通のコンテキストのもとで関連するコンテンツをまとめます。タブストリップには利用できるビュー全体が表示され、タブを切り替えるとページ遷移なしでコンテンツパネルが入れ替わります。すべてのビューに共通の見出し、主要なアクション、または共通のテーマがある場合に最も効果的です。互いに関係のないタブは、別々のページに分けましょう。
タブを使う場合と使わない場合
| タブを使う場合… | 別の方法を使う場合… |
|---|---|
| 2〜7個のビューに共通のコンテキストがある(同じレコード、同じ設定セクションなど) | タブが7〜8個を超える場合 — 代わりにサイドバーまたはセクションナビゲーションを使います |
| セッション中にユーザーがビューを頻繁に切り替える | コンテンツが順序立っている場合(代わりにステッパー/ウィザードを使います) |
| すべてのタブが同等の有効な入口である | 明らかに主要なビューが1つある場合 — 残りはセカンダリナビゲーションまたはオーバーフローに配置します |
| ビューに共通のヘッダーまたはページレベルのアクションがある | 各「タブ」に異なるヘッダーとレイアウトが必要な場合 — ページを分けます |
タブの種類
レイアウトのコンテキストに合ったビジュアルスタイルを選びます。
下線/インジケータータブ
アクティブなタブに、スライドする下線または下罫線が付いた横並びのストリップです。最も控えめなスタイルで、コンテンツ領域内にタブストリップを配置するページレベルのタブに適しています。
Overview | Activity | Settings
──────────
コンテインド/ボックスタブ
各タブが独立したボックスになっており、アクティブなタブは下のパネルにつながっているように見えます。視覚的な存在感が強く、画面上部やカード内に目立つタブグループを配置する場合に適しています。
ピル/ボタンタブ
状態を切り替える角丸のカプセル型タブです。強調度が低く、セクション内のセカンダリコンテンツを切り替える場合(ページレベルではない場合)に適しています。ビューの切り替え(例:「リスト/グリッド/マップ」)によく使われます。
構造
┌─────────────────────────────────────────────────┐
│ Tab A │ Tab B │ Tab C │ │ ← Tab strip (role="tablist")
├─────────┴─────────────────────────────────────────┤
│ │
│ Tab panel content │ ← Tab panel (role="tabpanel")
│ │
└───────────────────────────────────────────────────┘
- タブストリップ: 高さは固定(通常40〜48px)です。ページと一緒にスクロールしません — ページが長い場合は、固定表示を検討します。
- タブパネル: 利用可能な残りの高さを埋めます。コンテンツがあふれたときにスクロールするのは、タブストリップではなくパネルです。
- アクティブインジケーター: 下線タブでは
--color-primaryの2〜3pxの下罫線、コンテインド/ピルタブでは塗りつぶし背景です。
状態
| 状態 | ビジュアルスタイル |
|---|---|
| アクティブ | プライマリカラーのインジケーター。ラベルは --color-text-primary またはプライマリカラー |
| 非アクティブ | 控えめなラベル(--color-text-secondary)。インジケーターなし |
| ホバー | 背景がわずかに変化(--color-grey-50)。ラベルが少し濃くなる |
| フォーカス | 視認できるフォーカスリング(outline: 2px solid --color-primary; outline-offset: 2px) |
| 無効 | 不透明度を下げる(0.4)。cursor: not-allowed。アクティブなタブにはしない |
アクティブなタブを無効にしてはいけません。コンテンツを利用できない場合は、タブを無効にするのではなく、説明とともにパネル内に表示します。
タブのオーバーフロー
タブストリップがコンテナーより広い場合、タブを複数行に折り返さないでください。ストリップという視覚的な表現が損なわれます。対処方法は3つあります。優先度の高い順に示します。
セレクトへのフォールバック
ストリップを計測し、コンテンツがコンテナーより広い場合は、同じ選択肢を含むネイティブな<select>を表示します。端から見切れた項目がなく、どの幅でもすべての選択肢をひと目で確認して選べます。また、タッチ操作のユーザーにはプラットフォーム標準のピッカーが提供されます。
スクロール可能なストリップ
ストリップを水平方向にスクロールできるようにし、右端にフェードを付けてオーバーフローを示します。モバイルで一般的なパターンで、タブの一覧をユーザーが把握している場合には適していますが、初めて読む人には選択肢が隠れてしまいます。
.tab-strip {
display: flex;
overflow-x: auto;
scrollbar-width: none; /* hide scrollbar visually on desktop */
}
.tab-strip::after {
content: '';
position: absolute; right: 0;
background: linear-gradient(to left, var(--color-surface), transparent);
pointer-events: none;
}
「その他」オーバーフローメニュー
収まるだけのタブを表示し、残りはMore ▾ドロップダウンにまとめます。オーバーフローしたタブがアクティブな場合は、「More」のラベルを更新します:Settings ▾(アクティブな非表示タブの名前を表示します)。
どの方法を使う場合でも、選択中のタブだけに下線を付けます。ストリップ全体の下に線を引くと、タブの下線と競合します。ビューが多いデスクトップダッシュボードでは、オーバーフロータブよりサイドバーナビゲーションを優先します。
垂直タブ
タブが5個以上あり、レイアウトに左サイドバーがある場合に使用します。垂直タブなら、ラベルを長くしてもはみ出しにくく、より柔軟に拡張できます。
- 固定幅のサイドバー(通常は200~240px)にタブラベルを縦に並べます
- アクティブなタブ:左ボーダーのアクセント(
3px solid --color-primary)+控えめな背景色 - メインコンテンツ領域は残りの幅を使います
キーボード操作
タブでは、ストリップ内の移動にARIAの「ロービング tabindex」パターンを使用します。
| キー | 操作 |
|---|---|
Tab | タブストリップにフォーカスを移動します(続いてアクティブなパネルに移動します) |
← / → | タブ間を移動します(水平ストリップ)。コンテンツの読み込みコストに応じて、即座にアクティブにするか、Enterキーでアクティブにします |
↑ / ↓ | タブ間を移動します(垂直ストリップ) |
Home | 最初のタブに移動します |
End | 最後のタブに移動します |
Enter / Space | フォーカス中のタブをアクティブにします(矢印キーで自動的にアクティブにならない場合) |
自動アクティブ化と手動アクティブ化: タブの切り替えでネットワークリクエストが発生する場合は、不要なフェッチを避けるため、手動アクティブ化を使用します(矢印キーでフォーカスを移動し、Enterキーでアクティブにします)。コンテンツがすでに読み込まれている場合や、読み込みコストが低い場合は、矢印キーで自動的にアクティブにしてもかまいません。
ARIA
<div role="tablist" aria-label="Product sections">
<button
role="tab"
id="tab-overview"
aria-selected="true"
aria-controls="panel-overview"
tabindex="0"
>Overview</button>
<button
role="tab"
id="tab-activity"
aria-selected="false"
aria-controls="panel-activity"
tabindex="-1"
>Activity</button>
</div>
<div
role="tabpanel"
id="panel-overview"
aria-labelledby="tab-overview"
>…</div>
<div
role="tabpanel"
id="panel-activity"
aria-labelledby="tab-activity"
hidden
>…</div>
- アクティブなタブには
aria-selected="true"、それ以外にはfalseを設定します - アクティブなタブには
tabindex="0"、それ以外には-1を設定します(ロービング tabindex) - 非アクティブなパネルには
hidden属性(またはdisplay: none)を設定します。スクリーンリーダーは非表示のパネルを読み飛ばします - 上の見出しだけではグループを十分に説明できない場合は、
tablistにaria-labelを設定します
状態の保持
ページの読み込みや移動をまたいで、アクティブなタブを保持します。
- URLフラグメントまたはクエリパラメーター:
?tab=activity。最も堅牢な方法で、ディープリンクとブラウザーの戻る/進む操作に対応します - localStorage: 共有する必要のない設定(ダッシュボードの優先表示など)に使用できます
sessionStorageは使用しないでください。タブを閉じると状態が予期せず失われます
URLにタブのパラメーターが含まれている場合、最初のタブでなくても、読み込み時にそのタブへ移動します。
ネストされたタブ
タブの中にタブを配置するのは避けてください。タブをネストすると、どの範囲を操作しているのかが分かりにくくなります。「タブA › サブタブ2」でコンテンツを編集するユーザーにとって、外側のタブが何を意味するのか、明確なイメージを持てなくなります。
タブをネストしている場合は、情報設計を見直してください。
- 内側のタブをセカンダリナビゲーション(ピル、パネル内のサイドバー)にできますか?
- 外側のタブを最上位のページナビゲーションにできますか?
メインコンテンツ領域でのタブは、最大1階層にしてください。
レビューチェックリスト
- タブは2~7個で、共通の主題またはコンテキストを共有していますか?
- 対象となるすべてのビューポートで、タブストリップが複数行に折り返されませんか?
- アクティブなタブは、色やインジケーターで明確に区別されていますか?
- ホバー時とフォーカス時の状態は、コントラスト要件を満たしていますか?
- 無効なタブを避け、利用できないことはパネル内に表示していますか?
- キーボード操作はロービング tabindexに従っていますか(←/→でタブ間を移動し、Tabでパネルに移動)?
-
role="tablist"、role="tab"、role="tabpanel"、aria-selectedは正しく設定されていますか? - 非アクティブなパネルは、アクセシビリティツリーから非表示になっていますか(
hidden属性)? - アクティブなタブはURL(ディープリンク可能なビューの場合)またはlocalStorageに保持されていますか?
- ネストされたタブを避けていますか?
- タブがあふれる場合は、セレクト、スクロール可能なストリップ、または「その他」メニューを使用し、折り返しはしていませんか?
- 選択中のタブだけに下線が表示され、ストリップ全体の下には線が引かれていませんか?