'input type="datetime-local" step base の落とし穴'

2026-09-07 rails html

Rails のある編集画面で「時刻を 13:00:00 にしたいのに、ブラウザが "正しい値を選択してください。最も近い正しい値は 13:00:42 です" のような端数秒を 勧めてくる」という現象に出くわしました。原因を追うと <input type="datetime-local">step base(刻みの基準点) の仕様に行き着きましたので、最小再現込みでまとめます。

症状

環境

TL;DR(原因)

  1. Rails の datetime_fieldtype="datetime-local" を出力します。
  2. DatetimeLocalFieldinclude_seconds が既定で true なので、value 属性を strftime("%Y-%m-%dT%T") すなわち秒まで含めて描画します (value="2026-06-17T07:46:08")。
  3. step を指定していないので、datetime-local の暗黙の刻みは 60 秒です。
  4. min 属性が無いと、HTML 仕様上 step base(刻みの起点)は value 属性そのもの になります。
  5. その結果、この入力欄が受け付けるのは「もとの値の秒(:08)に一致する時刻」 だけです。13:00:00 は刻みに乗らず不正となり、"最も近い正しい値" は 12:59:0813:00:08 になります。

つまり利用者が壊したわけではなく、「秒付き value + step 未指定 + min 無し」の 組み合わせで、最初に入っていた値の秒が、その欄の許容値をロックしてしまう という話です。

<input type="datetime-local">step の仕様

stepdatetime-local / time では秒単位で、未指定なら既定 60 です。 値が妥当かどうかは次で判定されます(WHATWG HTML Standard)。

value − stepBasestep(×スケール係数)の整数倍であること

そして step base の決まり方は以下のとおりです。

  1. min 属性があってパースできれば、それが step base
  2. さもなくば value 属性があってパースできれば、それが step base
  3. どちらも無ければ既定値(datetime-local は 0 相当)

今回は min が無いので 2 が採用され、step base = 2026-06-17T07:46:08 に なります。step = 60s なので許容値は …:46:08 から 60 秒間隔、すなわち 「毎分 08 秒」の時刻だけになります。

補足: Chrome が秒スピナーを表示するのは step < 60 のときに加えて、 value が端数秒を持つときもです。そのため「未指定なのに秒が編集できる」状態に なり、かえって混乱します。

最小再現

repro.html(dev サーバの public/ に置いて http://localhost:3000/ 配下で配信):

<!doctype html>
<meta charset="utf-8">
<title>datetime-local step base repro</title>

<h3>Rails datetime_field 相当(include_seconds: true = 既定)</h3>
<form>
  <input type="datetime-local" name="a" value="2026-09-07T13:00:42">
  <button>submit</button>
</form>

<h3>include_seconds: false 相当</h3>
<form>
  <input type="datetime-local" name="b" value="2026-09-07T13:00">
  <button>submit</button>
</form>

<h3>step: 1 相当</h3>
<form>
  <input type="datetime-local" name="c" value="2026-09-07T13:00:42" step="1">
  <button>submit</button>
</form>

<script>
document.querySelectorAll('form').forEach(f => f.addEventListener('submit', e => {
  e.preventDefault();
  const i = f.querySelector('input');
  document.title = i.name + ': valid=' + i.checkValidity() + ' msg=' + i.validationMessage;
}));
</script>

checkValidity() / validationMessage を JS から叩いて挙動を観察しました。

ケース A: value="2026-09-07T13:00:42", step 未指定(=現状の Rails 出力)

入れた値 checkValidity() validationMessage
2026-09-07T13:00:42(初期値のまま) true (空)
2026-09-07T13:00:00 false Please enter a valid value. The two nearest valid values are 2026/09/07, 12:59:42 and 2026/09/07, 13:00:42.
2026-09-07T13:05:00 false ... nearest valid values are 2026/09/07, 13:04:42 and 2026/09/07, 13:05:42.
2026-09-07T13:05:42(秒を初期値に合わせる) true (空)

「毎分 42 秒」の時刻しか通りません。日本語 UI の Chrome では 「正しい値を選択してください。最も近い正しい値は …:42 です」に相当します。

ケース B: value="2026-09-07T13:00"include_seconds: false 相当)

入れた値 checkValidity()
2026-09-07T13:00 true

秒スピナーが消え、step base も :00 になるので普通に分単位で入ります。

ケース C: value="2026-09-07T13:00:42", step="1"

入れた値 checkValidity()
2026-09-07T13:00:00 true

1 秒刻みなので任意の秒が有効です。利用者が秒を 00 に直せます。

実アプリでの確認

修正前の状態を実データ(value="2026-06-17T07:46:08")で再現しました。

入れた値 checkValidity() メッセージ
2026-06-17T13:00:00 false ... nearest valid values are 2026/06/17, 12:59:08 and 2026/06/17, 13:00:08.
2026-06-17T13:00:08 true

step="1" を付けた後は次のとおりです。

入れた値 checkValidity()
2026-06-17T13:00:00 true

レンダリング結果:

<input class="form-control" step="1" value="2026-06-17T07:46:08"
       type="datetime-local" name="entry[started_at]" id="entry_started_at">

対処法

案 A: include_seconds: false

<%= form.datetime_field :started_at, class: "form-control", include_seconds: false %>

案 B: step: 1(今回採用)

<%= form.datetime_field :started_at, class: "form-control", step: 1 %>
<%= form.datetime_field :ended_at,   class: "form-control", step: 1 %>

今回は「もとの秒を保ったまま必要なときだけ直せる」ほうが運用に合っていたので 案 B を採用しました。

これは誰の問題か —— Rails ヘルパーの既定の不整合

厳密には datetime-local 単体の仕様の話ではなく、Rails の datetime_field / time_field が既定で出力する「秒付き value + step 未指定」という組み合わせが 引き金です。actionview 8.1 のソースを見ると次のようになっています。

ヘルパー include_seconds の既定 value のフォーマット step
datetime_fieldDatetimeLocalField true %Y-%m-%dT%T(秒あり) 出力しない
time_fieldTimeField true %T.%L(秒+ミリ秒あり) 出力しない

step を出さなければブラウザの既定は 60 秒刻みです。つまり「秒(time_field は ミリ秒まで)を含む value を出しておきながら、その精度を許可する step は出さない」 状態で、min が無く秒が 00 でなければレンダリングした時点で stepMismatch に なります。自分が入れた value を自分で不正判定する形で、これは仕様というより デフォルトの設計上の隙です。

なぜ step: 1 を既定にしないのか。Rails は value のフォーマットと step を別の 関心事として扱っているように見えます。include_seconds の既定が true なのは、 false にすると送信時に秒が黙って切り捨てられて値が壊れるため(値の往復を担保 する)。一方 step は入力の粒度・バリデーション方針であり、アプリが決めるもので ヘルパーは押し付けない、という立場です。step="1" を出すとブラウザ UI も変わり (秒スピナーを強制)、全ユーザーにそれを課すことになります。

とはいえ include_seconds: false のサポート自体が Rails 7.1(2022〜2023 頃)と 比較的新しく、ドキュメントも「秒を外すとブラウザによっては簡単な UI になる」まで しか触れておらず、stepMismatch には言及がありません。実務上は「秒を持つカラムに datetime_field / time_field を素で使うと、既定のままでは不正になり得る入力欄が できる」footgun だと捉えておくのが安全です。

教訓