複数の選択肢を視覚的なボタングループとして並べ、排他選択(単一選択)または複数選択を提供するUIコンポーネント。テキストエディタのフォント装飾(B / I / U)・表示切り替え(グリッド / リスト)・フィルタータグの選択などで広く使われる。
この記事を読むと、Toggle GroupとRadio・CheckboxとTabs・Filterの使い分け・単一選択と複数選択のAPI設計・必ず1つ選択済みにする強制選択モード・ARIA rolegroup + aria-pressed パターンが自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
複数の選択肢をボタングループとして並べ、単一選択(排他)または複数選択を提供するUIコンポーネント。フォームの <input type="radio"> や <input type="checkbox"> の「ボタン版」として機能する。role="group" でグループを意味論的にまとめ、各ボタンに aria-pressed で選択状態を伝える。
RadioとToggle Groupの違い:Radioはフォーム内の「値の送信」を目的とし、<fieldset> + <legend> で構造化する。Toggle Groupはフォーム外のUIの「状態制御」(表示切り替え・フィルター・テキスト装飾)を目的とし、role="group" + aria-pressed で実装する。
TabsとToggle Groupの違い:Tabsは「表示するパネルコンテンツを切り替える」——選択したタブに対応するコンテンツブロックが表示される。Toggle Groupは「UIの状態を制御する」——コンテンツブロックとの紐付けはなく、状態の値を管理するだけ。
3. 使い分け(When to use / When NOT to use)
3.1 When to use
- 表示形式の切り替え(グリッド表示 / リスト表示 / テーブル表示)
- テキストエディタのフォント装飾(太字 / イタリック / 下線)
- フィルタータグ(複数選択可能なカテゴリフィルター)
- 設定のオプション切り替え(サイズ: S / M / L、整列: 左 / 中央 / 右)
3.2 When NOT to use
- フォームで値を送信する →
Radio(単一)またはCheckbox(複数)を使う - 選択肢が6個以上 →
SelectまたはComboboxを使う - コンテンツパネルの切り替え →
Tabsを使う(aria-controlsでパネルと紐付ける)
3.3 代替UI(Alternatives)
- フォームの単一選択 →
Radio Button - フォームの複数選択 →
Checkbox - コンテンツ切り替え →
Tabs - 多数の選択肢 →
Select/Combobox
4. 設計判断の核(Decision Principles)
Toggle Groupの核は「単一選択 vs 複数選択」の明確な設計——どちらのモードかをUIの見た目と動作で一貫させる。排他選択なのに0件選択できてしまうと、ユーザーはどの状態が「デフォルト」か分からなくなる。
判断の優先順位:① 単一/複数選択モードの決定 → ② 強制選択(0件禁止)の要否 → ③ スタイリング → ④ ARIA実装
- 単一選択は「必ず1つ選択済み」を強制する:表示形式の切り替えなど、「未選択」という状態が存在しないケースでは、選択済みのボタンを再クリックしても選択解除されないようにする(
if (newValue === current) return;) - 複数選択は0件を許容するかどうかを明示する:フィルタータグでは0件選択=「すべて表示」として機能させるのが自然。テキスト装飾では0件=「装飾なし」として許容する
- グループ全体に
role="group"とaria-labelを付与する:role="group"でスクリーンリーダーに「関連するボタンのグループ」として認識させる。aria-labelで「テキスト揃え」「カテゴリフィルター」など目的を伝える - 各ボタンに
aria-pressedを付与する:aria-pressed="true"で選択中、aria-pressed="false"で非選択を伝える。aria-checkedではなくaria-pressedを使う(<input type="radio">との意味論的な違いを保つ)
5. 状態設計(States)
5.1 必須状態(Required)
- Default(非選択):
aria-pressed="false"、未選択スタイル - Pressed(選択中):
aria-pressed="true"、ハイライトスタイル
5.2 条件付き状態(Conditional)
- Disabled(無効):グレーアウト、
disabled属性 またはaria-disabled="true" - Hover / Focus:ホバーハイライト、フォーカスリング
5.3 State Gallery
| 状態 | 必須 | 何を伝えるか |
|---|---|---|
| Default(非選択) | ✅ | このオプションは現在選択されていない |
| Pressed(選択中) | ✅ | このオプションが現在選択されている |
| Disabled | — | この選択肢は現在使えない |
6. バリエーション設計(Variants)
選択モードとスタイルの組み合わせで使い分ける。
| バリアント | 選択モード | スタイル | 使用例 |
|---|---|---|---|
| セグメントコントロール | 単一・強制選択 | ピル型・背景色切り替え | 表示形式・ソート順 |
| ボタングループ | 単一または複数 | ボーダー・色切り替え | テキスト装飾・整列 |
| フィルタータグ | 複数・0件許容 | ピル型・ラウンド | カテゴリフィルター |
| アイコングループ | 単一または複数 | アイコンのみ | テキスト揃え・フォーマット |
禁止パターン:Toggle Groupをフォームの送信値の管理に使う → <input type="radio"> / <input type="checkbox"> を使い、フォーム送信に正しく対応させる。
7. パターン集(Good / Bad / How to fix)
7.0 よく崩れる設計パターン(3つ)
- 単一選択で0件選択が可能:「グリッド表示」「リスト表示」の切り替えで、選択済みを再クリックすると両方が非選択になり、どの表示になっているか分からなくなる
- aria-pressed がない:スクリーンリーダーがどのボタンが選択中かを伝えられず、視覚に頼れないユーザーが選択状態を把握できない
- アイコンのみで aria-label がない:「B」「I」「U」のみのボタングループで、スクリーンリーダーが「B ボタン」としか読み上げず「太字」を伝えられない
7.1 Bad(典型3つ)
onClick={() => setSelected(val === selected ? null : val)}で0件選択を許容し、表示形式が「未選択」という不正状態になる<button className={selected === val ? 'active' : ''}>のスタイル切り替えのみでaria-pressedがない<button>B</button>だけでaria-label="太字"もtitle="太字"も付与されていない
7.2 Good(対になる3つ)
onClick={() => { if (val !== selected) setSelected(val); }}で再クリック時の選択解除を防ぎ、常に1つが選択済みの状態を保つ- 各ボタンに
aria-pressed={selected === val}を付与し、スクリーンリーダーに「押されています」「押されていません」を伝える - アイコンのみのボタンには
aria-label="太字"とtitle="太字"の両方を付与し、スクリーンリーダーとツールチップの両方に対応する
7.3 How to fix(手順)
- 単一選択:
onClickにif (val === current) return;を追加して選択解除を防ぐ - 各ボタンに
aria-pressed={value === item.value}を追加する - グループ全体のラッパーに
role="group"とaria-label="[グループの目的]"を追加する - アイコンのみのボタンに
aria-labelを追加する
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Keyboard
Tabでグループにフォーカス←・→(または↑・↓)で項目間を移動する(roving tabindex: フォーカス中の項目のみtabIndex={0}、それ以外はtabIndex={-1})Space/Enterで選択/解除
Focus
- フォーカスリングをすべてのボタンに表示する
- roving tabindex を使うと「Tabでグループ全体→矢印で項目移動」の自然なキーボード体験になる
Screen Reader
<div role="group" aria-label="テキスト揃え">
<button aria-pressed="true" aria-label="左揃え" tabindex="0">≡</button>
<button aria-pressed="false" aria-label="中央揃え" tabindex="-1">☰</button>
<button aria-pressed="false" aria-label="右揃え" tabindex="-1">≡</button>
</div>
Touch / Pointer
- ボタンのタップ領域は最低44×44px
- ピル型の小さいフィルタータグでは
min-height: 44px+ 十分な水平パディングで確保する
10. 実装メモ(Implementation Notes)
- shadcn/ui の
ToggleGroupは@radix-ui/react-toggle-groupベースで、type="single"/type="multiple"の切り替え・aria-pressed・roving tabindex がすべて実装済み - roving tabindex の自前実装は
useRefで全ボタンの参照を保持し、onKeyDownでArrowRight/ArrowLeftを捕捉してフォーカスを移動する。実装コストが高いため shadcn/ui の使用を強く推奨 - フィルタータグとして使う場合、選択状態をURLのクエリパラメータ(
?tags=ui-components,glossary)に永続化すると、ページリロード・ブラウザバックでフィルター状態が復元される
11. 関連リンク
- 関連するUIデザイン原則: 視覚的階層 (Visual Hierarchy), フィードバック (Feedback)
- 用語集(定義): アクセシビリティ (Accessibility)
- 関連するUIコンポーネント(横): Filter(フィルター), Tabs(タブ), Toggle(トグルボタン)
12. まとめ
Toggle Groupの設計で最重要なのは「単一 vs 複数選択モードの明確化」と「aria-pressed の実装」です。迷ったら 4. 設計判断の核 に戻り、単一選択での強制選択・role="group" + aria-label・aria-pressed の3点を確認してください。shadcn/ui の ToggleGroup を使えばroving tabindex・aria-pressed・単一/複数選択モードは自動的に実装されます。RadioやCheckboxとの混同に注意——フォームへの値送信が目的なら <input type="radio"> / <input type="checkbox"> を使い、UIの状態制御が目的なら Toggle Group を使うという使い分けが正しい選択です。