Rails のある編集画面で「時刻を 13:00:00 にしたいのに、ブラウザが
"正しい値を選択してください。最も近い正しい値は 13:00:42 です" のような端数秒を
勧めてくる」という現象に出くわしました。原因を追うと <input type="datetime-local">
の step base(刻みの基準点) の仕様に行き着きましたので、最小再現込みでまとめます。
HH:MM:00 が弾かれます。…:42(もとの値の端数秒)」
と出ます。00 にすると弾かれます。form.datetime_field を使用しています。
erb
<%= form.datetime_field :started_at, class: "form-control" %>
<%= form.datetime_field :ended_at, class: "form-control" %>
Time.current を使って自動的に書き込まれるため、
値はほぼ必ず端数秒を持ちます(例 2026-06-17T07:46:08)。あとから人が
この編集画面で修正する、という運用です。datetime_field は type="datetime-local" を出力します。DatetimeLocalField は include_seconds が既定で true なので、value 属性を
strftime("%Y-%m-%dT%T") すなわち秒まで含めて描画します
(value="2026-06-17T07:46:08")。step を指定していないので、datetime-local の暗黙の刻みは 60 秒です。min 属性が無いと、HTML 仕様上 step base(刻みの起点)は value 属性そのもの
になります。:08)に一致する時刻」
だけです。13:00:00 は刻みに乗らず不正となり、"最も近い正しい値" は 12:59:08 と
13:00:08 になります。つまり利用者が壊したわけではなく、「秒付き value + step 未指定 + min 無し」の 組み合わせで、最初に入っていた値の秒が、その欄の許容値をロックしてしまう という話です。
<input type="datetime-local"> の step の仕様step は datetime-local / time では秒単位で、未指定なら既定 60 です。
値が妥当かどうかは次で判定されます(WHATWG HTML Standard)。
value − stepBaseがstep(×スケール係数)の整数倍であること
そして step base の決まり方は以下のとおりです。
min 属性があってパースできれば、それが step basevalue 属性があってパースできれば、それが step basedatetime-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 から叩いて挙動を観察しました。
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 です」に相当します。
value="2026-09-07T13:00"(include_seconds: false 相当)| 入れた値 | checkValidity() |
|---|---|
2026-09-07T13:00 |
true |
秒スピナーが消え、step base も :00 になるので普通に分単位で入ります。
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">
include_seconds: false<%= form.datetime_field :started_at, class: "form-control", include_seconds: false %>
value が 2026-06-17T07:46(秒なし)になり、秒スピナーも消えます。HH:MM がそのまま通ります。step: 1(今回採用)<%= form.datetime_field :started_at, class: "form-control", step: 1 %>
<%= form.datetime_field :ended_at, class: "form-control", step: 1 %>
00 に直せます。今回は「もとの秒を保ったまま必要なときだけ直せる」ほうが運用に合っていたので 案 B を採用しました。
厳密には datetime-local 単体の仕様の話ではなく、Rails の datetime_field /
time_field が既定で出力する「秒付き value + step 未指定」という組み合わせが
引き金です。actionview 8.1 のソースを見ると次のようになっています。
| ヘルパー | include_seconds の既定 |
value のフォーマット | step |
|---|---|---|---|
datetime_field(DatetimeLocalField) |
true |
%Y-%m-%dT%T(秒あり) |
出力しない |
time_field(TimeField) |
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 だと捉えておくのが安全です。
datetime-local / time の value に端数秒を入れるなら step も必ず指定
します。さもないと「最初の値の秒」が刻みの起点になり、以後その秒でしか入力
できなくなります。min を置くと step base はそちらに移ります。min があるフォームで同じ症状が
出たら min の端数秒を疑ってください。datetime_field / time_field は既定で秒付き(include_seconds: true)
なのに step を出しません(前節)。秒を持つカラムに使うときは、秒がいらないなら
include_seconds: false、秒を扱うなら step: を必ず添えます。