日付の選択・予定の表示・期間の選択を行う高度な複合UIコンポーネント。Date Picker(単一日付選択)・Event Calendar(予定表示)・Date Range Picker(期間選択) の3種類がある。日付という「時間の概念」をグリッドで視覚化し、ユーザーが直感的に日付を認識・選択できるようにする。予約システム・スケジューラ・フォームの日付入力など幅広い場面で使われる。
この記事を読むと、3種類のCalendarの使い分け・月ナビゲーションの設計・Date Range Pickerの開始/終了日の管理・キーボードナビゲーション(矢印キー移動)・role="grid" と aria-selected の実装が自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
日付を月単位のグリッドで視覚化し、選択・閲覧・入力を可能にする複合UIコンポーネント。内部的にはグリッド(7列 × 最大6行)・ナビゲーションボタン・曜日ヘッダー・日付セルの4つのサブコンポーネントで構成される。
3種類のCalendar:
| 種類 | 目的 | 選択 | 使用例 |
|---|---|---|---|
| Date Picker | 単一日付の選択 | 1日 | フォームの誕生日・期日入力 |
| Date Range Picker | 期間の選択 | 開始〜終了 | ホテル予約・レポート期間 |
| Event Calendar | 予定の閲覧・表示 | なし/単一 | スケジューラ・カレンダーアプリ |
3. 使い分け(When to use / When NOT to use)
3.1 When to use
- Date Picker:フォームで日付の入力が必要な場面(予約日・生年月日・期日)
- Date Range Picker:期間の指定が必要な場面(宿泊期間・フィルター期間・レポート範囲)
- Event Calendar:予定・イベントをカレンダービューで閲覧する場面
3.2 When NOT to use
- 年・月のみの選択:
<input type="month">またはカスタムのMonth Pickerの方が適切 - 遠い過去・未来の日付入力(例:生年月日の1980年代):テキスト入力または年・月・日のセレクト3つの方が操作が速い
- 日付入力が省略できる場合:カレンダーは重いUIなので、日付入力が必須でなければ省略する
4. 設計判断の核(Decision Principles)
Calendarの核は「キーボードナビゲーション」——カレンダーはマウスで操作するUIに見えるが、矢印キーで日付を移動・選択できることがアクセシビリティの要件。これなしではキーボードユーザーとスクリーンリーダーユーザーが完全に排除される。
判断の優先順位:① キーボードナビゲーション(矢印キー) → ② role="grid" + aria-selected → ③ 月ナビゲーションの見出し aria-live → ④ 無効日付の設計
- 矢印キーで日付を移動できるようにする:
onKeyDownでArrowLeft/Right/Up/Downを処理し、それぞれ前日/翌日/1週間前/1週間後にフォーカスを移動する。月をまたぐ移動も適切に処理する role="grid"+ 各セルにrole="gridcell"+aria-selected:カレンダーグリッド全体にrole="grid"を付与し、日付セルにrole="gridcell"+aria-selected="true/false"を設定する。aria-labelで「2026年3月5日」のように日付を読み上げられるようにする- 月ナビゲーション後に
aria-live="polite"で月を読み上げる:「前の月」「次の月」ボタンを押した後、月の見出しが変わったことをスクリーンリーダーに伝える。見出し要素にaria-live="polite"を設定する - 無効日付(past dates / disabled dates)を
aria-disabled="true":過去の日付や選択不可の日付にはaria-disabled="true"を設定し、視覚的に非活性化する。pointer-events: noneと合わせて実装する
5. 状態設計(States)
5.1 日付セルの状態
| 状態 | 視覚的表示 | aria |
|---|---|---|
| 通常 | デフォルトスタイル | aria-selected="false" |
| 今日 | リング・太字 | aria-current="date" |
| 選択済み | 青塗り | aria-selected="true" |
| 範囲内(Range) | 薄い青背景 | aria-selected="true" |
| 無効 | グレーアウト | aria-disabled="true" |
| 当月外 | 薄い表示 | role="presentation" |
| フォーカス | フォーカスリング | tabindex="0" |
5.2 月ナビゲーション
- 前月・次月ボタン:
aria-label="前の月"/aria-label="次の月" - 月の見出し:
aria-live="polite"で月変更を通知
6. バリエーション設計(Variants)
| バリアント | 選択モード | 特記事項 |
|---|---|---|
| Single Date Picker | 1日 | フォームに組み込む場合は Popover と組み合わせる |
| Date Range Picker | 開始〜終了 | 開始日・範囲内・終了日で3種類の視覚スタイルが必要 |
| Multi-select | 複数の任意の日 | シフト管理・複数日程選択 |
| Event Calendar(月表示) | なし/単一 | セル内に予定ドット・タイトルを表示 |
| Event Calendar(週表示) | なし/単一 | 時間軸グリッドで時間帯ごとに表示 |
7. パターン集(Good / Bad / How to fix)
7.0 よく崩れる設計パターン(3つ)
- マウス専用のカレンダー:
onClickのみ実装し、キーボードで日付を移動・選択できない - 月ナビゲーション後にフォーカスが消える:「次の月」ボタンを押した後、フォーカスがどこかに飛んでしまう
- Date Range Pickerで選択中状態が不明:開始日をクリック後、終了日をクリックするまで何が選択されているか分からない
7.1 Bad(典型3つ)
- 日付セルが
<div onClick>で実装されており、Tabキーでフォーカスできず矢印キーでも移動できない role="grid"がなく、スクリーンリーダーが単なるテキストの羅列として読み上げる- Date Range Pickerで開始日クリック後、終了日にマウスを乗せても選択範囲がプレビューされない
7.2 Good(対になる3つ)
- グリッドに
role="grid"+ 各セルにrole="gridcell" tabIndex={focusedDate === date ? 0 : -1}でrovingTabIndexを実装し、矢印キーで移動できる - 月ナビゲーション後、月の見出しに
aria-live="polite"を付与し、「2026年4月」と読み上げられる - Range選択中、マウスホバーした日付まで選択範囲をプレビュー表示し「どこまで選択されるか」を可視化する
7.3 How to fix(手順)
- 日付セルの
<div>を<button>に変えるかrole="gridcell" tabIndexを設定して、キーボードフォーカス可能にする - グリッドに
role="grid"+ 月見出しにaria-live="polite"を追加する onKeyDownでArrowキーの処理を実装し、フォーカスを移動する- Range選択中の
onMouseEnterでプレビュー状態を管理する
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Keyboard Navigation
| キー | 動作 |
|---|---|
← / → | 前日 / 翌日 |
↑ / ↓ | 1週間前 / 1週間後 |
Enter / Space | 日付を選択 |
PageUp / PageDown | 前月 / 次月 |
Home / End | 月の最初 / 最後の日 |
Screen Reader
<div role="grid" aria-label="2026年3月のカレンダー">
<div role="row">
<div role="columnheader" abbr="日曜日">日</div>
<!-- ... -->
</div>
<div role="row">
<div
role="gridcell"
aria-label="2026年3月1日"
aria-selected="false"
tabindex="-1"
>1</div>
<div
role="gridcell"
aria-label="2026年3月2日、今日"
aria-current="date"
aria-selected="true"
tabindex="0"
>2</div>
</div>
</div>
10. 実装メモ(Implementation Notes)
- shadcn/ui の
Calendarはreact-day-pickerライブラリのラッパー。キーボードナビゲーション・ARIA対応・Date Range Pickerがすべて含まれており、カスタムCalendarを1から実装するより推奨される react-day-pickerのmode="single"/mode="range"/mode="multiple"で3種類の選択モードを切り替えられる- フォームと組み合わせる場合は
Popover+Calendarのパターンが一般的(shadcn/ui のDate Pickerの実装例がこのパターン)。Popover内にCalendarを配置し、選択後にPopoverを閉じてフォームフィールドに値を反映する - キーボードナビゲーションを自前実装する場合:
rovingTabIndexパターンを使い、フォーカス中の日付だけtabIndex={0}、それ以外はtabIndex={-1}にすることで、Tabキーでカレンダー全体を1つのウィジェットとして扱える
11. 関連リンク
- 関連するUIデザイン原則: フィードバック (Feedback), 誤操作を防ぐ (Error Prevention)
- 用語集(定義): アクセシビリティ (Accessibility)
- 関連するUIコンポーネント(横): Popover(ポップオーバー), Text Input(テキスト入力), Select(セレクト), Scroll Area(スクロールエリア)
12. まとめ
Calendarの設計で最重要なのは「キーボードナビゲーション」と「role="grid" + aria-selected のARIA実装」です。Calendarはビジュアル重視になりがちですが、矢印キーで日付を移動できることはWCAGが要求する最低限の実装です。shadcn/ui の Calendar(react-day-picker ベース)を使うと、これらのアクセシビリティ要件がすべて実装済みのため、特別な理由がない限りは自前実装よりライブラリの活用を推奨します。