日付ピッカー
入力欄をタップするとカレンダーが開き、日を選ぶと入力される。月の前後移動にも対応。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- 予約日・誕生日など特定の1日を確定させる入力
- 日付の書式を覚えさせたくないフォーム
- 今日と選択日を目立たせたい小さなカレンダー
- 避ける場面
- 数年離れた日付(年選択が無く前月連打になる)
- 開始〜終了の範囲選択
- 構成要素
- ラベル
- 入力欄(readonly)
- カレンダー
- 前月・翌月ボタン
- 曜日見出し
- 日付ボタン
- 持っている状態
- 通常
- ホバー
- フォーカス
- 押下中
- キーボード
入力欄で Enter/Space/↓ で開き、その月の日へフォーカス。←→ で ±1日、↑↓ で ±1週(月をまたぐ)、Home/End で月初・月末、Enter で選択、Esc で閉じて入力欄へ戻る
- 支援技術
label[for] で名前を結び、入力欄に aria-haspopup=dialog・aria-expanded。カレンダーは role=dialog+aria-label、月名は aria-live=polite。日は roving tabindex。選択日は塗り+aria-pressed
- 動き
日付の背景 120ms、ナビ背景 150ms(押せる場所を示す)。reduced-motion で無効
デザインの要点
- 月初の曜日ぶんだけ空セルを敷いてから日を並べる。曜日見出しと列が揃わないカレンダーは読めない
- 今日は太字+青、選択日は塗りつぶし。同じ見た目にすると「今日を選んだ」のか「今日はどこか」が分からない
- 日付グリッドは roving tabindex にして Tab は1回で抜ける。30 個のボタンを順に Tab させない
- 選んだら閉じて入力欄へフォーカスを戻す。開いたままだと次の欄へ進むのに一手多い
使いどころ
生年月日や予約日のように「特定の1日」を確定させたい入力に向く。カレンダーを見ながら選べるため、日付の書式(YYYY/MM/DDかMM/DD/YYYYか)を覚える必要がなく、入力ミスの入り込む余地も減る。この実装の input は readonly 属性が付いており、直接タイプでの入力は最初から想定されていない。したがって「今日から10年後の日付」のように離れた月まで前月/翌月ボタンだけで移動する必要がある使い方には向かない(クイックジャンプや年選択が無いため、たとえば2010年生まれを選ぶには数十回「前月」を押すことになる)。日付の範囲選択(開始日〜終了日)にも対応しておらず、範囲を扱うなら2つのピッカーを並べるか別途実装を拡張する必要がある。
仕組み
入力欄をクリックすると #dp-cal の hidden 属性をトグルしてカレンダーを開閉する。render() は cur.y/cur.m が指す月について、new Date(y, m, 1).getDay() で月初の曜日オフセットを求めて空セルを敷き詰め、new Date(y, m + 1, 0).getDate() でその月の日数を出して1日ずつボタンを生成する。today との一致で .today クラス、selected との一致で .selected クラスを付け替える。日付を押すと selected を更新し、YYYY/MM/DD形式(padStart(2,"0")で0埋め)を input.value に書き込んでカレンダーを閉じる。カレンダー外のクリックを document 全体の click リスナーで拾い、.dp-wrap の外側なら閉じる仕組みになっている。
実装の注意
最も見落としやすいのは、input が readonly なので「打てない欄」に見えることである。クリックだけでなく Enter/Space/↓ でも開くように keydown を張ってあるが、この処理を削ると Tab で input に到達しても開けなくなり、キーボードのみのユーザーはカレンダーに到達できない(テキスト入力に Enter はデフォルトで何も起こさない)。次に、日付ボタンは毎月 render() で作り直すため、フォーカスを持っていた日が消えて body へ落ちる。矢印キーで月をまたいだときは render(true) で新しい月の該当日へ明示的に focus() を移す必要があり、これを忘れると「↓を押したらフォーカスが消えた」という壊れ方をする。またカレンダーは position: absolute で入力欄の直下に固定表示されるため、画面下端に近い位置で開くとカレンダー本体が画面外にはみ出す可能性があり、上に開く反転ロジックは実装されていない。
アクセシビリティ
カレンダー全体に role="dialog" と aria-label(「日付を選択」)を付け、input には aria-haspopup="dialog" と aria-expanded で開閉状態を示している。前月/翌月ボタンは aria-label(「前月」「翌月」)、月名の #dp-month は aria-live="polite" なので月を送るたびに「2026年10月」と読み上げられる。日付ボタンは roving tabindex(その月でフォーカス可能な1日だけ tabindex=0、他は -1)にしてあり、Tab はカレンダーを1回で抜け、日の移動は ←→(±1日)・↑↓(±1週)・Home/End(月初・月末)で行う。月末で→を押すと翌月の1日へ移り、そこへフォーカスも移動する。選択日は .selected の塗りに加えて aria-pressed="true" を付け、今日は太字と色で示す。Escape で閉じると input へフォーカスが戻る。WAI-ARIA Authoring Practices の Date Picker Dialog に近い形だが、role="grid"(row/gridcell の階層)までは付けていないので、行単位の読み上げが要る場合はそこを足す。
調整して使う
readonly をやめて自由入力+blur時のパースに変える(その場合、開くキーは ↓ だけに絞り Enter/Space は入力に使う)、最小/最大日付(min/max)を設けて過去日や特定範囲外を選択不可にする(予約フォームで過去日を防ぐ用途)、週の始まりを日曜から月曜に変える(現状のグリッド見出しは日始まり固定)、「今日」へのショートカットボタンを足す、といった拡張が考えられる。範囲選択が必要な場合は selected を単一の {y,m,d} ではなく開始・終了の2値に拡張し、範囲内の日をハイライトするロジックを render() に追加する形になる。カレンダーの向きも、入力欄が画面下部にある場合は上方向に開くよう出し分けると崩れを防げる。
コード(コピーして使えます)
<div class="dp-wrap">
<label class="dp-label" for="dp-input">日付</label>
<div class="dp-field-wrap">
<input
id="dp-input"
class="dp-input"
type="text"
placeholder="YYYY/MM/DD"
readonly
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="dp-cal"
/>
<span class="dp-icon" aria-hidden="true">📅</span>
</div>
<div class="dp-cal" id="dp-cal" role="dialog" aria-label="日付を選択" hidden>
<div class="dp-cal-head">
<button class="dp-nav" id="dp-prev" type="button" aria-label="前月">‹</button>
<span class="dp-month" id="dp-month" aria-live="polite"></span>
<button class="dp-nav" id="dp-next" type="button" aria-label="翌月">›</button>
</div>
<div class="dp-grid-head" aria-hidden="true">
<span>日</span><span>月</span><span>火</span><span>水</span><span>木</span><span>金</span><span>土</span>
</div>
<div class="dp-grid" id="dp-grid"></div>
</div>
</div>