SAKURUG TECHBLOG

ブラウザデフォルトのカレンダーUI調整をしてみよう。

timestampauthor-name
Haruya

はじめに

この記事では、<input type="date> の標準カレンダーにおけるUIカスタマイズについてご紹介します。

ややニッチな内容ではありますが、jQuery導入の検討材料となるよう、実装可能な範囲と限界についても整理しました。

UI設計の際の参考にしていただければ幸いです。

デフォルトカレンダーの概要

MDN Web Docsでは、<input type="date">について以下のように説明されています。

<input> 要素の type="date" は、ユーザーが日付を入力できる入力フィールドを作成します。 日付選択入力 UI の表示は、ブラウザーやオペレーティングシステムによって異なります。 値は yyyy-mm-dd 形式に正規化されます。

結果の値には年、月、日が含まれますが、時刻は含まれません。 time および datetime-local 入力型は時刻や日付と時刻の入力に対応しています。

https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/input/date

実際に配置すると、ブラウザごとに以下のようなアイコンやカレンダーが表示されます。


アイコンをクリックし、カレンダーが開いた状態↓

今回は、この標準カレンダーを利用しつつ、挙動を制御するUI調整を行います。

注意するべきポイント

①各ブラウザでのUI差分

MDNの説明にもある通り、日付選択UIはブラウザやOSに依存します。 ほぼ全てのモダンブラウザ(IE終了後)に対応しており、値(value)も yyyy-mm-dd 形式で統一されていますが、見た目については各環境で異なります。

日付選択入力 UI の表示は、ブラウザーやオペレーティングシステムによって異なります。


【UIの違いに関しては以下記事を参照】

https://qiita.com/ruteshi_SI_shiteru/items/78cca55a8133b076fb6d


デフォルトのカレンダーをカスタマイズする場合、対応するOSやブラウザの仕様をあらかじめ把握しておくことが重要です。

②複雑な要件は実装しにくい

カレンダー部分は「Shadow DOM」という内部構造に格納されているため、「特定の祝日を選択不可にする」「カレンダー内に独自のボタンを追加する」といった複雑な操作は困難です。

要件に高度なカスタマイズが含まれる場合は、jQuery UI DatepickerFlatpickr などのライブラリ使用を検討するのが現実的です。


実現したい挙動


目的: 「入力日」と「視聴日」の入力項目が混在しているフォームにおいて、ユーザーが誤って両方に同じ日付を入れてしまうミスを防ぎたい。そのため、視聴日を選択した直後に「確認モーダル」を表示させます。

処理の流れ:

ユーザーが日付を選択した瞬間に確認モーダルを表示。

「はい」の場合: 確定し、次の項目へ自動遷移しないようフォーカスを外す(blur)。

「いいえ」の場合: 値をクリアし、再度カレンダーを自動で開く。

ユーザーフロー図↓

実装




<script>
  document.addEventListener('DOMContentLoaded', function() {
    const inputDate = document.getElementById("input_1");
    const modal = document.getElementById("confirmModal");
    const confirmText = document.getElementById("confirmText");
    const btnYes = document.getElementById("btnYes");
    const btnNo = document.getElementById("btnNo");

    let isConfirmed = false;

    // イベント発火用関数
    function triggerValidation() {
      inputDate.dispatchEvent(new Event('input', { bubbles: true }));
      inputDate.dispatchEvent(new Event('change', { bubbles: true }));
    }

    // 日付が変更された時の処理
    inputDate.addEventListener("change", function () {
      if (inputDate.value === "" || isConfirmed) {
        isConfirmed = false;
        return;
      }

      const dateObj = new Date(inputDate.value);
      const displayDate = `${dateObj.getFullYear()}${dateObj.getMonth() + 1}${dateObj.getDate()}日`;
      
      confirmText.textContent = `選択された日付は ${displayDate} です。\nこの日付で間違いないですか?`;
      modal.style.display = "flex";
    });

    // 「はい」ボタンの処理
    btnYes.addEventListener("click", function () {
      isConfirmed = true; 
      modal.style.display = "none";
      
      // バリデーションを実行
      triggerValidation();

      // 【追加】次の要素(他input)への自動フォーカスを阻止する
      // フォームシステムの自動遷移スクリプトより後に実行されるよう、ごく僅かに遅延させる
      setTimeout(() => {
        if (document.activeElement) {
          document.activeElement.blur(); // 現在フォーカスが当たっている要素(他input)からフォーカスを外す
        }
        inputDate.blur(); // 視聴日項目自体のフォーカスも外す
      }, 50); 
    });

    // 「いいえ」ボタンの処理
    btnNo.addEventListener("click", function (e) {
      e.preventDefault();
      
      isConfirmed = false;
      modal.style.display = "none";
      inputDate.value = ""; 
      
      inputDate.focus();
      triggerValidation();

      try {
        if (typeof inputDate.showPicker === "function") {
          inputDate.showPicker();
        } else {
          inputDate.click();
        }
      } catch (error) {
        console.error("カレンダーの表示に失敗しました:", error);
        inputDate.focus();
      }
    });
  });
</script>
<!-- 視聴日inputここから -->
<div class="form_layout" id="form_day">
  <div class="form_label">
      <label>視聴日</label>
  </div>
  <div class="form_body">
      <div class="form_inner">
          <div class="form">
              <input type="date" name="1" size="20" maxlength="10000" required="required" value=""  id="input_1">
          </div>
      </div>
  </div>
</div>
<!-- //視聴日inputここまで -->

<!-- モーダルここから -->
<div id="confirmModal" class="modal">
  <div class="modal-content">
      <p id="confirmText" class="confirmTextBody">この日付で間違いないですか?</p>
      <div class="btn-box">
          <button id="btnNo" class="btn-gray">いいえ</button>
          <button id="btnYes" class="btn-blue">はい</button>
      </div>
  </div>
</div>
<!-- //モーダルここまで -->


<!--  CSS  -->

  <style>
    /* モーダル全体 */
.modal {
    display: none;
    position: fixed;
    z-index: 9999;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.3);
    justify-content: center;
    align-items: center;
}

/* モーダル中身 */
.modal-content {
    background-color: rgba(255, 255, 255, 0.9);
    padding: 40px 20px;
    border-radius: 20px;
    width: 85%;
    max-width: 400px;
    text-align: center;
    box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
</style>
<!--  //CSS  -->

実装解説

1. 初期設定と要素の取得

まず、DOMが完全に読み込まれた後に、操作対象となるHTML要素(日付入力欄、モーダル、ボタンなど)を変数に格納します。

const inputDate = document.getElementById("input_1");
// ...他、モーダルやボタンの取得
let isConfirmed = false; // 二重発火を防ぐためのフラグ

isConfirmed フラグ: 後の「はい」ボタン押下時にも change イベントが発生し、無限ループに陥るのを防ぐために重要な役割を果たします。

2. 外部連携のための「イベント手動発火」

この関数は、プログラムから値を変更した際、フォームシステム側(バリデーション機能など)に「変更があった」ことを通知するために用意しております。

function triggerValidation() {
 inputDate.dispatchEvent(new Event('input', { bubbles: true }));
inputDate.dispatchEvent(new Event('change', { bubbles: true }));
}

dispatchEvent: 通常、JavaScriptから input.value を書き換えても change イベントは自動で発生しません。そのため、手動でイベントを飛ばすことで、システム側に「入力が完了した」と認識させています。

3. 日付変更の検知

ユーザーがカレンダーから日付を選んだ瞬間に動くメインロジックです。

inputDate.addEventListener("change", function () {
 if (inputDate.value === "" || isConfirmed) {
    isConfirmed = false; // フラグをリセット
    return;
  }
  // 日付のフォーマット処理とモーダル表示
  const dateObj = new Date(inputDate.value);
  const displayDate = ${dateObj.getFullYear()}年${dateObj.getMonth() + 1}月${dateObj.getDate()}日;
  confirmText.textContent = 選択された日付は ${displayDate} です。\nこの日付で間違いないですか?;
  modal.style.display = "flex";
});

change イベント: 入力が確定したタイミングで発火します。

表示の工夫: inputDate.value は通常 YYYY-MM-DD 形式ですが、ユーザーが読みやすいように 〇〇年〇月〇日 形式に変換してモーダルに表示しています。

4. 「はい」ボタンの処理

btnYes.addEventListener("click", function () {
 isConfirmed = true; 
  modal.style.display = "none";
  triggerValidation();
  // 【重要】自動フォーカス遷移の阻止
  setTimeout(() => {
    if (document.activeElement) {
      document.activeElement.blur(); 
    }
    inputDate.blur(); 
  }, 50); 
});

setTimeout(..., 50): 多くのフォームシステム(Jotform等)は、項目入力後に自動で「次の入力欄」にフォーカスを移動させます。

blur() による強制解除: フォームシステムの自動遷移スクリプトが動いた直後に、意図的に blur()(フォーカスを外す)を実行することで、勝手に画面がスクロールしたり、次の項目がアクティブになったりするのを防いでいます。

(利便性のためにフォーカスを外していますが、アクセシビリティの観点からキーボード操作ユーザーにとっては注意が必要です)

5. 「いいえ」ボタンの処理

入力をやり直したい場合、値を消すだけでなく、再度カレンダーを開く処理をしております。

btnNo.addEventListener("click", function (e) {

  e.preventDefault();

  isConfirmed = false;

  modal.style.display = "none";

  inputDate.value = ""; // 入力値をクリア

  

  inputDate.focus();

  triggerValidation();

  try {

    if (typeof inputDate.showPicker === "function") {

      inputDate.showPicker(); // プログラムからカレンダーを開く

    } else {

      inputDate.click();

    }

  } catch (error) {

    console.error("カレンダーの表示に失敗しました:", error);

  }

});

showPicker(): 近年のモダンブラウザ(Chrome等)で追加されたAPIです。JSから「カレンダーを強制的に展開する」ことができるため、ユーザーがもう一度入力欄をクリックする手間を省いています。

実装してみての感想。

デフォルトのカレンダーは制約が多いものの、その仕様を逆手に取ってどう要件を満たすか検討するのは、ブラウザの挙動を深く知る良い機会になりました。 案件に厳しい縛りがなければ、最初からライブラリ(jQuery等)を導入するのが近道ですが、標準機能の限界を知ることで、より最適な技術選定ができるようになると感じました。

この記事が、どなたかのお役に立てれば幸いです。


ーーーーーーーーーー
株式会社SAKURUGは「寄付月間2025」に参画しています。
12月中のテックブログのpv数に応じて、アフリカの支援団体に寄付をおこないます。
https://giving12.jp/
ーーーーーーーーーー

▼高校生向けインターン実施中!

弊社では高校生向けにインターンを行っております!
現役エンジニア指導の下、一緒に働いてみませんか?

高校生インターン応募フォーム

▼カジュアル面談実施中!

カジュアル面談では、会社の雰囲気や仕事内容についてざっくばらんにお話ししています。
履歴書は不要、服装自由、原則オンラインです。興味を持っていただけた方は、
ぜひ以下からお申し込みください。

皆さんにお会いできることをサクラグメンバー一同、心より楽しみにしております!

カジュアル面談応募フォーム

記事をシェアする

ABOUT ME

author-image
Haruya
準備中