アクセシビリティ
すべてのピッカーコンポーネントは、組み込みのアクセシビリティサポートを備えています。キーボードナビゲーション、ARIA属性、フォーカス管理は、Styled、Tailwind v3/v4、Headlessのすべてのバリアントで、追加設定なしですぐに機能します。
標準的な使用では、追加の設定は必要ありません。このページでは、カスタムUIでパターンを監査、拡張、または複製できるように、含まれている機能について説明します。
キーボードナビゲーション
カレンダーピッカー
| キー | アクション |
|---|---|
ArrowLeft | フォーカスを前の日に移動 |
ArrowRight | フォーカスを次の日に移動 |
ArrowUp | フォーカスを前の週に移動(同じ曜日) |
ArrowDown | フォーカスを次の週に移動(同じ曜日) |
Enter / Space | フォーカスされている日を選択 |
Escape | 変更をキャンセルしてポップアップを閉じる |
Tab | 次のフォーカス可能な要素に移動(ブラウザのデフォルト) |
無効な日付のスキップ: 矢印キーでナビゲーションする際、無効な日付は自動的にスキップされます。ピッカーは、移動方向に最大365日間検索して、次の有効な日付を見つけます。
月の自動スクロール: キーボードフォーカスが別の月の日付に移動すると、カレンダービューはその月を表示するように更新されます。
タイムピッカー
| キー | アクション |
|---|---|
Escape | 変更をキャンセルしてポップアップを閉じる |
時刻の選択には、クリック/タッチ操作を伴うスクロールホイールUIを使用します。スクロールカラムは、正確な値を選択するために scroll-snap を使用します。
ARIA属性
すべてのインタラクティブな要素には、適切なARIAロールと属性があります:
ポップアップとコンテナ
| 要素 | 属性 | 値 |
|---|---|---|
| ポップアップコンテナ | role | "dialog" |
| インラインコンテナ | role | "group" |
| コンテナ | aria-label | ロケールからのプレースホルダーテキスト |
| コンテナ | aria-activedescendant | 現在フォーカスされている日付セルのID |
トリガーボタン
| 属性 | 値 |
|---|---|
aria-expanded | ポップアップが開いているときに true |
aria-haspopup | "dialog" |
カレンダーグリッド
| 要素 | 属性 | 値 |
|---|---|---|
| グリッドラッパー | role | "grid" |
| 週の行 | role | "row" |
| 曜日のヘッダーセル | role | "columnheader" |
| 日付セル | role | "gridcell" |
| 日付セル | aria-selected | 選択されているときに true |
| 日付セル | aria-current | セルが今日の場合に "date" |
| 日付セル | aria-label | フォーマットされた日付文字列(例: "2026-03-04") |
| 日付セル | disabled | 無効な日付に設定(ネイティブHTML属性) |
ナビゲーションとドロップダウン
| 要素 | 属性 | 値 |
|---|---|---|
| 前の月のボタン | aria-label | 「前の月」 (locale.prevMonthLabel から) |
| 次の月のボタン | aria-label | 「次の月」 (locale.nextMonthLabel から) |
| 月のドロップダウン | aria-label | 「月を選択」 (locale.selectMonthLabel から) |
| 年のドロップダウン | aria-label | 「年を選択」 (locale.selectYearLabel から) |
時刻カラム
| 要素 | 属性 | 値 |
|---|---|---|
| 時間カラム | role | "listbox" |
| 時間カラム | aria-orientation | "vertical" |
| 時間カラム | aria-label | 「時間」 (locale.hourLabel から) |
| 分カラム | role | "listbox" |
| 分カラム | aria-orientation | "vertical" |
| 分カラム | aria-label | 「分」 (locale.minuteLabel から) |
| 秒カラム | role | "listbox" |
| 秒カラム | aria-orientation | "vertical" |
| 秒カラム | aria-label | 「秒」 (locale.secondLabel から) |
| 時刻アイテム | role | 「option」 (パディングアイテム: 「presentation」) |
| 時刻アイテム | aria-selected | アイテムが現在の値である場合に true |
| AM/PM切り替え | aria-label | 現在の期間 + 「、AM/PMを切り替え」 |
| クリアボタン | aria-label | 「クリア」 (locale.clear から) |
すべてのラベル文字列は、ロケールシステムを通じてカスタマイズ可能です。
フォーカス管理
Roving Tabindex
カレンダーグリッドは roving tabindex パターンを使用します:
- 現在フォーカスされている日付セルのみが
tabIndex={0}を持ちます(タブで移動可能)。 - 他のすべての日付セルは
tabIndex={-1}を持ちます(タブでは移動できませんが、矢印キーでフォーカス可能)。 - これは、
Tabキーがカレンダーグリッドからフォーカスを(次のコントロールへ)移動させ、矢印キーがグリッド内でフォーカスを移動させることを意味します。
ポップアップのフォーカストラップ
ポップアップモードは、@floating-ui/react の FloatingFocusManager を使用して、以下を実現します:
- ポップアップが開いている間、フォーカスをその中にトラップします。
- ポップアップが閉じられたときに、フォーカスをトリガーボタンに戻します。
- ポップアップから背景コンテンツへのタブ移動を防ぎます。
ネイティブ要素
月と年の選択には、本質的にキーボードでアクセス可能なネイティブの <select> 要素を使用します。日付セルには、適切な無効状態を持つネイティブの <button> 要素を使用します。
WCAG 2.1準拠
以下のWCAG 2.1成功基準に対応しています:
| 基準 | レベル | 対応方法 |
|---|---|---|
| 1.3.1 情報及び関係性 | A | セマンティックHTML — <button>, <select>, grid/gridcellロール |
| 2.1.1 キーボード | A | すべての機能がキーボード(矢印キー、Enter、Escape)で到達可能 |
| 2.4.3 フォーカス順序 | A | 論理的なタブ順序。ポップアップはフォーカスマネージャーを使用 |
| 2.4.7 フォーカスの可視化 | AA | すべてのインタラクティブな要素に :focus-visible のアウトライン(2pxの実線、プライマリーカラー) |
| 4.1.2 名前、役割、値 | A | すべてのインタラクティブな要素は aria-label またはテキストコンテンツを介してアクセス可能な名前を持つ |
スタイリングパッケージ(Styled, Tailwind v3/v4)には、デフォルトで可視のフォーカスインジケータが含まれています。headlessパッケージでカスタムUIを構築する場合は、独自のフォーカススタイルを提供する必要があります。
Headless: アクセシブルなカスタムUIの構築
headlessパッケージを直接使用している場合は、カスタムUIに以下が含まれていることを確認してください:
1. キーボードハンドラを適用する
矢印キーによるナビゲーションが機能するように、ポップアップコンテナに handleKeyDown を渡します:
const { handleKeyDown, ... } = useDatePicker({ value, onChange });
<div onKeyDown={handleKeyDown}> {/* calendar grid */}</div>2. 日付セルに正しいtabIndexを設定する
getDayProps() からの isFocused フラグを使用します:
const dayProps = getDayProps(date);
<button role="gridcell" tabIndex={dayProps.isFocused ? 0 : -1} aria-selected={dayProps.isSelected} aria-current={dayProps.isToday ? "date" : undefined} aria-label={locale.formatDate(date)}> {dayProps.day}</button>;3. グリッドにARIAロールを追加する
<div role="grid" aria-label={locale.placeholder}> <div role="row"> {locale.weekdays.map((wd) => ( <div key={wd} role="columnheader"> {wd} </div> ))} </div> {calendar.weeks.map((week, i) => ( <div key={i} role="row"> {/* day cells with role="gridcell" */} </div> ))}</div>完全なウォークスルーについては、カスタムUIの構築を参照してください。