TimePicker 2 — Flatpickr ライブラリ版

フォーム 中級

このコンポーネントについて

人気ライブラリ Flatpickr を使った時刻選択コンポーネントです。 入力欄をクリックするとポップアップが表示され、時・分のスピナーを操作するだけで素早く時刻を選べます。 バニラJS版と比べてコードが大幅に短くなり、CDNを1本追加するだけで動く手軽さが特徴です。

時刻のみを選ぶ単体選択(Pattern 1)と、開始〜終了の時間帯を2つのピッカーで選ぶ範囲選択(Pattern 2)の2パターンを収録しています。 時刻の選択はリアルタイムで反映され、「決定」ボタンで確定後の値取得フローも確認できます。 予約フォームや勤怠入力など、時刻指定が必要な場面に広く応用できます。

📎 外部ライブラリを使わず、バニラJSのスピナー型で作った版は TimePicker 1 — 時刻選択(バニラJS版) をご覧ください。 コードの仕組みを学びたい方・ライブラリ導入が難しい環境の方におすすめです。
  • 時刻ポップアップ選択 — 入力欄またはアイコンボタンのクリックで Flatpickr のポップアップを表示。時・分スピナーで操作
  • 24時間表記time_24hr: true で 0〜23 時の24時間形式で表示
  • 日本語ロケールlocale: 'ja' で Flatpickr の UI を日本語化(ja.min.js ロケールファイルを適用)
  • 分刻みインクリメントminuteIncrement: 1 で1分単位の操作。コードのコメント箇所を変更するだけで5分・15分刻みに切り替え可能
  • リアルタイム表示onChange コールバックで選択中の時刻を即時反映。値取得の仕組みが一目でわかる
  • 決定ボタン+値取得 — 「決定」ボタン押下で確定時刻を表示。リアルタイム反映と合わせて2通りの値取得パターンを学べる
  • 開始〜終了の制限(Pattern 2) — 開始時刻を選ぶと終了ピッカーの minTime が自動更新され、終了が開始より前を選べないよう制限

実装のポイント・注意点

Flatpickr の noCalendar: trueenableTime: true を組み合わせることで、カレンダーを表示せず時刻のみのポップアップになります。 デフォルトでは日付選択機能も含まれるため、時刻専用にしたい場合はこの組み合わせが必須です。

分の刻み幅minuteIncrement で制御します。デフォルト(1)では1分刻みですが、予約システムなど15分・30分単位で十分な場面では 1530 を指定すると選択しやすくなります。 サンプルソースの minuteIncrement: 1 の行にコメントで変更方法を記載しています。

Pattern 2 の時刻制限は、開始ピッカーの onChange 内で fpEnd.set('minTime', fpStart.input.value) を呼ぶことで実現しています。 開始をクリアした場合は fpEnd.set('minTime', null) で制限を解除します。 また、終了時刻がすでに新しい開始時刻以前に設定されていた場合は fpEnd.clear() で自動クリアし、矛盾した状態を防ぎます。

onChange と決定ボタンの使い分けonChange は選択のたびに呼ばれるリアルタイム反映向きで、フォームのプレビュー表示などに使います。 決定ボタン押下時の selectedDates からの値取得は、送信・確定操作のトリガーに向いています。 この2パターンを並べて学べることがこの事例の狙いです。

注意点:Flatpickr の clear() メソッドはデフォルトで onChange を呼び出しません。 そのため、クリアボタンのクリックハンドラ内でリアルタイム表示のリセット処理を明示的に書く必要があります(サンプルソースにコメントで記載)。

デモ

Pattern 1 — 時刻のみ選択

Pattern 2 — 開始〜終了時刻選択

開始時刻
終了時刻

開始: — / 終了: —

Powered by Flatpickr

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
Flatpickr の CSS・JS はネット接続時に CDN から自動取得されます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>TimePicker(Flatpickr)サンプル</title>
  <!-- Flatpickr CSS(CDN) -->
  <link rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css">
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<!-- Pattern 1: 時刻のみ選択 -->
<div class="tp-pattern">
  <p class="tp-pattern-label">Pattern 1 — 時刻のみ選択</p>
  <div class="tp-input-row">
    <input type="text" id="tp1-input" class="tp-input"
           placeholder="--:--" readonly autocomplete="off">
    <button type="button" class="tp-icon-btn" id="tp1-toggle" aria-label="時刻を選択">
      <svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
        <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
        <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
      </svg>
    </button>
    <button type="button" class="tp-clear-btn" id="tp1-clear" aria-label="クリア">×</button>
  </div>
  <p class="tp-realtime" id="tp1-realtime"></p>
  <button type="button" class="tp-confirm-btn" id="tp1-confirm">決定</button>
  <p class="tp-result" id="tp1-result"></p>
</div>

<!-- Pattern 2: 開始〜終了時刻選択 -->
<div class="tp-pattern">
  <p class="tp-pattern-label">Pattern 2 — 開始〜終了時刻選択</p>
  <div class="tp-time-row">
    <span class="tp-row-label">開始時刻</span>
    <div class="tp-input-row">
      <input type="text" id="tp2-start-input" class="tp-input"
             placeholder="--:--" readonly autocomplete="off">
      <button type="button" class="tp-icon-btn" id="tp2-start-toggle" aria-label="開始時刻を選択">
        <svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
          <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
        </svg>
      </button>
      <button type="button" class="tp-clear-btn" id="tp2-start-clear" aria-label="クリア">×</button>
    </div>
  </div>
  <div class="tp-time-row">
    <span class="tp-row-label">終了時刻</span>
    <div class="tp-input-row">
      <input type="text" id="tp2-end-input" class="tp-input"
             placeholder="--:--" readonly autocomplete="off">
      <button type="button" class="tp-icon-btn" id="tp2-end-toggle" aria-label="終了時刻を選択">
        <svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
          <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
        </svg>
      </button>
      <button type="button" class="tp-clear-btn" id="tp2-end-clear" aria-label="クリア">×</button>
    </div>
  </div>
  <p class="tp-realtime" id="tp2-realtime">開始: — / 終了: —</p>
  <button type="button" class="tp-confirm-btn" id="tp2-confirm">決定</button>
  <p class="tp-result" id="tp2-result"></p>
</div>

<!-- Flatpickr JS(本体 → 日本語ロケールの順で読み込む) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/ja.min.js"></script>
<script src="./script.js"></script>
</body>
</html>
/* === TimePicker(Flatpickr)パターン集 ===
   --tp-accent の値を変えるだけで配色を一括変更できます */
:root {
  --tp-accent: #2B7FE8; /* メインカラー(選択状態・ボタン等) */
  --tp-danger: #E53E3E; /* エラー色 */
  --tp-text:   #1A2332; /* メインテキスト色 */
  --tp-border: #D0D7E0; /* ボーダー色 */
  --tp-bg:     #F4F6F9; /* カード背景色 */
}

*, *::before, *::after { box-sizing: border-box; }
body {
  font-family: sans-serif;
  padding: 24px;
  max-width: 640px;
  margin: 0 auto;
  background: #fff;
  color: var(--tp-text);
}

/* パターンカード */
.tp-pattern {
  margin-bottom: 20px;
  padding: 20px;
  background: var(--tp-bg);
  border-radius: 8px;
}
.tp-pattern:last-child { margin-bottom: 0; }
.tp-pattern-label {
  margin: 0 0 14px;
  font-size: 11px;
  font-weight: 700;
  color: var(--tp-accent);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* 入力欄グループ(入力+アイコン+クリア) */
.tp-input-row { display: flex; align-items: center; }

/* 時刻行(Pattern 2:ラベル+入力欄グループ) */
.tp-time-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.tp-row-label {
  font-size: 13px;
  color: #666;
  width: 56px;
  flex-shrink: 0;
}

.tp-input {
  width: 80px;
  padding: 7px 10px;
  font-size: 14px;
  border: 1px solid var(--tp-border);
  border-radius: 4px 0 0 4px;
  outline: none;
  color: var(--tp-text);
  background: #fff;
  cursor: pointer;
  font-family: sans-serif;
}

.tp-icon-btn {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid var(--tp-border); border-left: none; border-radius: 0;
  background: #fff; color: #5A6A7A; cursor: pointer;
  transition: background 0.15s; flex-shrink: 0;
}
.tp-icon-btn:hover { background: var(--tp-bg); }

.tp-clear-btn {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 34px; padding: 0;
  border: 1px solid var(--tp-border); border-left: none;
  border-radius: 0 4px 4px 0;
  background: #fff; color: #9AA5B4; cursor: pointer;
  font-size: 14px; line-height: 1; transition: color 0.15s; flex-shrink: 0;
}
.tp-clear-btn:hover { color: #5A6A7A; }

/* リアルタイム表示 */
.tp-realtime {
  margin: 10px 0 0;
  font-size: 13px;
  color: #666;
  min-height: 20px;
}
.tp-realtime.has-value { color: var(--tp-accent); font-weight: 600; }

/* 決定ボタン */
.tp-confirm-btn {
  display: inline-block; margin-top: 14px; padding: 8px 24px;
  background: var(--tp-accent); color: #fff; border: none;
  border-radius: 6px; font-size: 14px; font-weight: 600;
  cursor: pointer; font-family: sans-serif; transition: opacity 0.15s;
}
.tp-confirm-btn:hover { opacity: 0.85; }

/* 結果表示 */
.tp-result { margin: 8px 0 0; font-size: 14px; min-height: 20px; }
.tp-result.has-value {
  padding: 10px 14px; background: #F0F7FF;
  border: 1px solid var(--tp-accent); border-radius: 6px;
  color: #1A5EA8; font-weight: 600;
}
.tp-result.is-error { color: var(--tp-danger); }

@media (max-width: 540px) {
  body { padding: 16px; }
  .tp-input { width: 72px; font-size: 13px; }
  .tp-row-label { width: 48px; font-size: 12px; }
}
// ===== Flatpickr 共通設定 =====
var baseConfig = {
  enableTime: true,          // 時刻選択を有効化
  noCalendar: true,          // カレンダーを非表示(時刻のみ)
  time_24hr: true,           // 24時間表記
  locale: 'ja',              // 日本語ロケール(l10n/ja.min.js 読み込み後に有効)
  dateFormat: 'H:i',         // 表示フォーマット: HH:mm
  minuteIncrement: 1         // 分の刻み幅(5分刻みにするには 5 に変更)
};


// ===== Pattern 1: 時刻のみ選択 =====
var fp1 = flatpickr('#tp1-input', Object.assign({}, baseConfig, {
  onChange: function(selectedDates) {
    // 時刻を選択するたびにリアルタイム表示を更新
    var el = document.getElementById('tp1-realtime');
    if (selectedDates.length > 0) {
      el.textContent = '選択中: ' + fp1.input.value;
      el.className = 'tp-realtime has-value';
    }
  }
}));

// アイコンボタンでポップアップを開く
document.getElementById('tp1-toggle').addEventListener('click', function() {
  fp1.open();
});

// ×ボタンで選択をリセット
// clear() は onChange を呼ばないため、リアルタイム表示を手動でリセット
document.getElementById('tp1-clear').addEventListener('click', function() {
  fp1.clear();
  var el = document.getElementById('tp1-realtime');
  el.textContent = '';
  el.className = 'tp-realtime';
  clearResult('tp1-result');
});

// 決定ボタンで結果を表示(selectedDates から値を取得するパターン)
document.getElementById('tp1-confirm').addEventListener('click', function() {
  var result = document.getElementById('tp1-result');
  if (fp1.selectedDates.length === 0) {
    showError(result, '※ 時刻を選択してください');
    return;
  }
  showValue(result, '選択時刻: ' + fp1.input.value);
});


// ===== Pattern 2: 開始〜終了時刻選択 =====
var fpStart = flatpickr('#tp2-start-input', Object.assign({}, baseConfig, {
  onChange: function(selectedDates) {
    if (selectedDates.length > 0) {
      // 終了ピッカーの最小時刻を開始時刻に設定
      fpEnd.set('minTime', fpStart.input.value);
      // 終了が開始以前なら自動クリア(矛盾した状態を防ぐ)
      if (fpEnd.selectedDates.length > 0 && fpEnd.selectedDates[0] <= selectedDates[0]) {
        fpEnd.clear();
      }
    } else {
      fpEnd.set('minTime', null); // 開始が未選択なら制限を解除
    }
    updatePattern2Realtime();
  }
}));

var fpEnd = flatpickr('#tp2-end-input', Object.assign({}, baseConfig, {
  onChange: function() {
    updatePattern2Realtime();
  }
}));

// 開始・終了のリアルタイム表示を更新(— は未選択を示す)
function updatePattern2Realtime() {
  var startVal = fpStart.input.value || '—';
  var endVal   = fpEnd.input.value   || '—';
  document.getElementById('tp2-realtime').textContent = '開始: ' + startVal + ' / 終了: ' + endVal;
}

document.getElementById('tp2-start-toggle').addEventListener('click', function() { fpStart.open(); });
document.getElementById('tp2-end-toggle').addEventListener('click', function()   { fpEnd.open(); });

// 開始クリア(clear() は onChange を呼ばないため手動でリセット)
document.getElementById('tp2-start-clear').addEventListener('click', function() {
  fpStart.clear();
  fpEnd.set('minTime', null); // 制限を解除
  updatePattern2Realtime();
  clearResult('tp2-result');
});

// 終了クリア
document.getElementById('tp2-end-clear').addEventListener('click', function() {
  fpEnd.clear();
  updatePattern2Realtime();
  clearResult('tp2-result');
});

// 決定ボタンで結果を表示
document.getElementById('tp2-confirm').addEventListener('click', function() {
  var result = document.getElementById('tp2-result');
  if (fpStart.selectedDates.length === 0 || fpEnd.selectedDates.length === 0) {
    showError(result, '※ 開始時刻と終了時刻の両方を選択してください');
    return;
  }
  showValue(result, '開始: ' + fpStart.input.value + ' / 終了: ' + fpEnd.input.value);
});


// ===== ユーティリティ =====
function showValue(el, text) {
  el.textContent = text;
  el.className = 'tp-result has-value';
}
function showError(el, text) {
  el.textContent = text;
  el.className = 'tp-result is-error';
}
function clearResult(id) {
  var el = document.getElementById(id);
  el.textContent = '';
  el.className = 'tp-result';
}

// デモリセットボタン用
function resetDemo() {
  fp1.clear();
  fpStart.clear();
  fpEnd.clear();
  var rt1 = document.getElementById('tp1-realtime');
  rt1.textContent = '';
  rt1.className = 'tp-realtime';
  fpEnd.set('minTime', null);
  updatePattern2Realtime();
  clearResult('tp1-result');
  clearResult('tp2-result');
}

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリのバージョン変更や要件の追記をして使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — 時刻のみ選択

# TimePicker(Flatpickr・時刻のみ選択)作成依頼

## 概要
Flatpickr ライブラリを使って、入力欄クリックでポップアップが表示される時刻選択コンポーネントを実装してください。

## 要件
- テキスト入力欄に時計アイコンボタン(SVG)を添えた見た目にする
- 入力欄クリック / アイコンボタンクリックで Flatpickr の時刻ポップアップを表示する
- 24時間表記(00:00〜23:59)で表示する
- 分の刻みは1分単位にする
- 日本語ロケール(locale: "ja")を適用する
- 時刻フォーマットは HH:mm(例: 09:30)にする
- 時刻を選択するたびに「選択中: HH:mm」をリアルタイム表示する(onChange コールバックで実装)
- ×クリアボタンで選択状態をリセットできるようにする
- 「決定」ボタン押下で選択時刻を「選択時刻: HH:mm」形式で表示する
- 未選択で決定ボタンを押した場合はエラーメッセージを表示する

## 技術仕様
- HTML / CSS / JavaScript で実装
- 外部ライブラリ:Flatpickr 4.6.13(CDN)+ 日本語ロケール(l10n/ja.min.js)
- レスポンシブ対応:必要

## 動作詳細
- Flatpickr オプション: enableTime: true / noCalendar: true / time_24hr: true / locale: "ja" / dateFormat: "H:i" / minuteIncrement: 1
- Flatpickr CSS: https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css
- Flatpickr JS: https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js
- 日本語ロケール(本体より後に読み込む): https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/ja.min.js
- リアルタイム表示は fp.input.value を使って文字列を生成する
- clear() は onChange を呼ばないため、クリアボタンのハンドラ内でリアルタイム表示を手動でリセットすること

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — 開始〜終了時刻選択

# TimePicker(Flatpickr・開始〜終了時刻選択)作成依頼

## 概要
Flatpickr ライブラリを使って、開始時刻と終了時刻を2つのピッカーで選択できる時間帯指定コンポーネントを実装してください。

## 要件
- 開始時刻・終了時刻それぞれに「テキスト入力欄+時計アイコンボタン+×クリアボタン」を用意する
- 入力欄クリック / アイコンボタンクリックで対応するポップアップを表示する
- 24時間表記、分1分刻み、HH:mm フォーマットにする
- 日本語ロケール(locale: "ja")を適用する
- 開始時刻を選択したら終了ピッカーの minTime を自動更新し、終了が開始より前を選べないよう制限する
- 開始時刻をクリアしたら終了ピッカーの minTime 制限を解除する
- 開始時刻を変更したとき、終了時刻が新しい開始時刻以前になっていたら終了時刻を自動クリアする
- 開始・終了の選択状態を「開始: HH:mm / 終了: HH:mm」形式でリアルタイム表示する(未選択は「—」と表示)
- 各クリアボタンで対応する入力欄をリセットする
- 「決定」ボタン押下で「開始: HH:mm / 終了: HH:mm」形式で表示する。どちらか未選択の場合はエラーメッセージを表示する

## 技術仕様
- HTML / CSS / JavaScript で実装
- 外部ライブラリ:Flatpickr 4.6.13(CDN)+ 日本語ロケール(l10n/ja.min.js)
- レスポンシブ対応:必要

## 動作詳細
- 開始・終了それぞれ独立した Flatpickr インスタンスを作成する(fpStart, fpEnd)
- 共通オプション: enableTime: true / noCalendar: true / time_24hr: true / locale: "ja" / dateFormat: "H:i" / minuteIncrement: 1
- fpStart の onChange 内で fpEnd.set('minTime', fpStart.input.value) を呼んで終了の最小時刻を更新する
- fpStart をクリアした場合は fpEnd.set('minTime', null) で制限を解除する
- Flatpickr CSS・JS・日本語ロケールの CDN URL は時刻のみ選択版と同じ URL を使用する
- clear() は onChange を呼ばないため、各クリアボタンのハンドラ内でリアルタイム表示を手動でリセットすること

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。