Reactで、Unix時間と日時を変換する入力欄を作る方法
Unix時間(1970年1月1日 00:00:00 UTC からの経過秒数)と日時の変換は、Date を間に挟めば数行で書けます。つまずきやすいのは日時の欄のほうです。<input type="datetime-local"> の値にはタイムゾーンが付いていないので、どのタイムゾーンの日時として読むかを、コードの側で決める必要があります。
ここでは、Unix時間の欄と日時の欄を並べ、どちらに入力しても、もう一方が書き換わる UnixTimeInput というコンポーネントを作りました。日時の欄は、ブラウザのタイムゾーンの日時として扱います。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。.tsx を esbuild で変換し、tsc で型エラーがないことを確かめたうえで、StrictMode で包んだ開発用のビルドを Playwright で操作しました。タイムゾーンは Asia/Tokyo と America/New_York の2つで試しています。Firefox と Safari、スマートフォンの日時の選択画面、スクリーンリーダーでの読み上げは試していません。

変換の関数
変換の処理は unixtime.ts にまとめました。
// Unix時間と、datetime-local の値(ブラウザのタイムゾーンの日時)を変換する関数
export type Unit = 's' | 'ms';
// この部品で扱う年(1年〜9999年。日時の欄の max 属性に合わせる)に収まるか
const inRange = (date: Date) => date.getFullYear() >= 1 && date.getFullYear() <= 9999;
const pad = (n: number, len = 2) => String(n).padStart(len, '0');
// Date を datetime-local に入れる文字列にする(toISOString は UTC なので使わない)
export function toLocalValue(date: Date, unit: Unit): string {
const day = `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
let time = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
if (unit === 'ms') time += `.${pad(date.getMilliseconds(), 3)}`;
return `${day}T${time}`;
}
// datetime-local の値を Date にする。日付と時刻の形で、時差を書かない文字列は
// その環境のタイムゾーンの日時として読まれる
export function fromLocalValue(value: string): Date | null {
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?$/.test(value)) return null;
const date = new Date(value);
return inRange(date) ? date : null;
}
export type UnixResult =
| { kind: 'empty' }
| { kind: 'ok'; date: Date; hint?: string }
| { kind: 'error'; message: string };
// 入力欄の文字列を Unix時間として読む
export function parseUnix(text: string, unit: Unit): UnixResult {
const s = text.normalize('NFKC').replace(/[\s,_]/g, '').replace(/^[−‐]/, '-');
if (s === '') return { kind: 'empty' };
if (!/^-?\d+$/.test(s)) return { kind: 'error', message: '整数で入力してください' };
const date = new Date(Number(s) * (unit === 's' ? 1000 : 1));
// いまの時刻は、秒なら10桁、ミリ秒なら13桁
const digits = s.replace('-', '').length;
const hint =
unit === 's' && digits >= 12 ? 'ミリ秒の値かもしれません' :
unit === 'ms' && digits <= 10 ? '秒の値かもしれません' : undefined;
if (!inRange(date)) {
const message = '1年から9999年までの日時になりません';
return { kind: 'error', message: hint ? `${message}。${hint}` : message };
}
return { kind: 'ok', date, hint };
}
export function toUnixText(date: Date, unit: Unit): string {
const ms = date.getTime();
return String(unit === 's' ? Math.floor(ms / 1000) : ms);
}
// その日時での、UTC との時差(+09:00 など)
export function offsetText(date: Date): string {
const m = -date.getTimezoneOffset();
return `${m < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(m) / 60))}:${pad(Math.abs(m) % 60)}`;
}
日時の欄の値は、ブラウザのタイムゾーンで読む
datetime-local の値は 2026-11-06T12:34 のような文字列で、時差(+09:00 や Z)が付きません。MDN の Date の「日時文字列形式」によると、時差のない文字列のうち、日付だけの形は UTC として、日付と時刻の形は現地時間として読まれます。fromLocalValue は、日付と時刻の形であることを正規表現で確かめてから new Date(value) に渡しています。
検証環境の Chromium(Asia/Tokyo)で試すと、new Date("2026-11-06T00:00") は 2026-11-05T15:00:00.000Z になり、日本時間の0時として読まれました。一方、new Date("2026-11-06") は 2026-11-06T00:00:00.000Z になり、UTC の0時として読まれました。日本時間の0時より9時間遅い時刻です。
逆向きの toLocalValue では、toISOString() を使っていません。toISOString() は、前に挙げた MDN の節にあるとおり UTC の日時を返すので、日本時間で入れたい欄に9時間前の日時が入ります。getFullYear() や getHours() などの現地時間の値を、ゼロ埋めしてつないでいます。
valueAsNumber は Unix時間ではない
入力欄から数値を取るなら valueAsNumber が手軽に見えますが、datetime-local の valueAsNumber は Unix時間としては使えません。HTML の仕様では、この欄の値は時差の情報を持たない日時とされ、数値にすると、1970-01-01T00:00:00.0 から入力された日時までのミリ秒数になります。入力された日時を、タイムゾーンのない日時のまま数えた値です。
検証環境の Chromium(Asia/Tokyo)で 2026-11-06T12:34 を入れると、次のように9時間の差が出ました。
| 取り方 | 値 | UTC で表すと |
|---|---|---|
valueAsNumber |
1793968440000 | 2026-11-06T12:34:00.000Z |
new Date(value).getTime() |
1793936040000 | 2026-11-06T03:34:00.000Z |
valueAsNumber は、入力した日時を UTC の日時とみなした値です。日本時間の12時34分の Unix時間は、下の行の値です。
秒とミリ秒を取り違えたときは、桁数でヒントを出す
Unix時間には秒とミリ秒の両方が使われます。JavaScript の Date.now() や getTime() が返すのは、MDN の Date の説明にあるとおりミリ秒単位の値です。単位はラジオボタンで選ぶようにし、桁数が合わないときにヒントを出しました。いまの時刻は、秒なら10桁、ミリ秒なら13桁です。
| 単位 | 入力 | 表示 |
|---|---|---|
| 秒 | 1793936040 | 2026-11-06T12:34(Asia/Tokyo) |
| 秒 | 1793936040000 | 1年から9999年までの日時になりません。ミリ秒の値かもしれません |
| ミリ秒 | 1793936096 | 1970-01-22T03:18:56.096(ヒント:秒の値かもしれません) |
| 秒 | abc | 整数で入力してください |
| 秒 | 1793936040(全角) | 2026-11-06T12:34 |
13桁の値を秒として読むと、9999年を超えるので範囲外のエラーになります。このエラーにも、同じヒントを添えています。全角の数字は normalize('NFKC') で半角にし、桁区切りのカンマや空白は replace で取り除いてから読みます。
HTML の仕様では、日時の欄の年は4桁以上の数字で、1以上であればよく、桁数の上限はありません。このコンポーネントでは、日時の欄の max 属性と fromLocalValue の正規表現で年を4桁(9999年まで)にしているので、Unix時間の側も1年から9999年に合わせました。Date 自体は、同じ MDN の節にあるとおり、1970年から前後1億日(西暦275760年まで)を表せます。
秒にするときは、ミリ秒を1000で割って Math.floor で切り捨てます。MDN の Date の例にも、経過した秒数を得るには Math.round ではなく Math.floor を使う、と書かれています。1970年より前は負の値になり、秒の -1 は、Asia/Tokyo では 1970-01-01T08:59:59 になりました。
入力欄のコンポーネントにまとめる
import { useId, useState } from 'react';
import { fromLocalValue, offsetText, parseUnix, toLocalValue, toUnixText, type Unit } from './unixtime';
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
export function UnixTimeInput({ initial = Date.now() }: { initial?: number }) {
const id = useId();
const [unit, setUnit] = useState<Unit>('s');
const [unixText, setUnixText] = useState(() => toUnixText(new Date(initial), 's'));
const [localText, setLocalText] = useState(() => toLocalValue(new Date(initial), 's'));
// 日時の欄が読めない(空になった)とき true
const [localError, setLocalError] = useState(false);
const unix = parseUnix(unixText, unit);
// 両方の欄が正しく読めているときだけ、その日時を使う
const date = !localError && unix.kind === 'ok' ? unix.date : null;
function changeUnix(text: string) {
setUnixText(text);
setLocalError(false);
const r = parseUnix(text, unit);
if (r.kind === 'ok') setLocalText(toLocalValue(r.date, unit));
}
function changeLocal(value: string) {
setLocalText(value);
const d = fromLocalValue(value);
setLocalError(d === null);
if (d) setUnixText(toUnixText(d, unit));
}
function changeUnit(next: Unit) {
setUnit(next);
// 同じ瞬間のまま、表し方だけを変える
if (date) {
setUnixText(toUnixText(date, next));
setLocalText(toLocalValue(date, next));
}
}
function setNow() {
const now = new Date();
setUnixText(toUnixText(now, unit));
setLocalText(toLocalValue(now, unit));
setLocalError(false);
}
return (
<div className="unixtime">
<fieldset>
<legend>単位</legend>
<label><input type="radio" name={`${id}-unit`} checked={unit === 's'} onChange={() => changeUnit('s')} /> 秒</label>
<label><input type="radio" name={`${id}-unit`} checked={unit === 'ms'} onChange={() => changeUnit('ms')} /> ミリ秒</label>
</fieldset>
<label htmlFor={`${id}-unix`}>Unix時間</label>
<input
id={`${id}-unix`}
inputMode="numeric"
value={unixText}
aria-invalid={unix.kind === 'error'}
aria-describedby={`${id}-unix-msg`}
onChange={(e) => changeUnix(e.target.value)}
/>
<p id={`${id}-unix-msg`} className={unix.kind === 'error' ? 'error' : 'hint'}>
{unix.kind === 'error' ? unix.message : unix.kind === 'ok' ? unix.hint : ''}
</p>
<label htmlFor={`${id}-local`}>日時({timeZone})</label>
<input
id={`${id}-local`}
type="datetime-local"
step={unit === 's' ? 1 : 0.001}
max="9999-12-31T23:59:59.999"
value={localText}
aria-invalid={localError && localText !== ''}
onChange={(e) => changeLocal(e.target.value)}
/>
<button type="button" onClick={setNow}>いまの時刻</button>
<output htmlFor={`${id}-unix ${id}-local`}>
{date ? `UTC:${date.toISOString()}(時差 ${offsetText(date)})` : ''}
</output>
</div>
);
}
.unixtime {
display: grid;
gap: 6px;
max-width: 360px;
}
.unixtime fieldset {
display: flex;
gap: 16px;
border: none;
padding: 0;
}
.unixtime input:not([type='radio']) {
font-size: 1.125rem;
padding: 6px 8px;
}
.unixtime p {
min-height: 1.5em;
margin: 0;
}
.unixtime .error {
color: #d85878;
}
.unixtime output {
color: #067a98;
font-family: monospace;
}
2つの欄の文字列を、それぞれ state に持っています。片方を編集したら、読めたときだけもう片方を書き換えます。Unix時間の欄に「abc」と打っても、日時の欄は直前の日時のまま残り、Unix時間の欄の aria-invalid だけが true になりました。下の UTC の行は、両方の欄が読めているときだけ出します。
日時の欄のラベルには、Intl.DateTimeFormat().resolvedOptions().timeZone で取ったタイムゾーンの名前を入れました。同じ Unix時間 1793936040 でも、Asia/Tokyo では 2026-11-06T12:34、America/New_York では 2026-11-05T22:34 と出ます。どこの日時なのかを、画面で分かるようにしています。
日時の欄の step で、秒とミリ秒を出す
datetime-local は、何も指定しなければ分までしか入力できません。MDN の datetime-local の説明にあるとおり、step の単位は秒で、既定は60(1分)です。単位が秒のときは step={1}、ミリ秒のときは step={0.001} にしました。ミリ秒を選んで 1793936096789 と打つと、日時の欄の値は 2026-11-06T12:34:56.789 になりました。
秒が0の時刻は、value から秒が消えます。検証環境の Chromium では、value に 2026-11-06T12:34:00 や 2026-11-06T12:34:00.000 を入れても、読み出すと 2026-11-06T12:34 でした。HTML の仕様で、秒が0なら秒を省いた、いちばん短い形にそろえると決められているためです。そこで、fromLocalValue の正規表現では秒を省略できるようにしました。
年を打っている途中も、値が変わる
日時の欄をキーボードで打つと、1文字ごとに onChange が呼ばれます。検証環境の Chromium で、年の欄に「2」「0」「2」「7」と打つと、値は次のように変わりました。
| 打った文字 | 日時の欄の value | Unix時間の欄 |
|---|---|---|
| 2 | 0002-11-06T12:34 | -62077351499 |
| 0 | 0020-11-06T12:34 | -61509271499 |
| 2 | 0202-11-06T12:34 | -55766004299 |
| 7 | 2027-11-06T12:34 | 1825472040 |
続けて月の欄で「0」を打つと、月が決まらないので value は空文字になりました。そのときは Unix時間の欄を書き換えず、UTC の行だけを消しています。「3」まで打つと、2027-03-06T12:34 の Unix時間に変わりました。
年が4桁になるまでの途中の値でも、Unix時間の欄が書き換わります。このとき、UTC の行の時差は「+09:18」になりました。tz データベースの asia ファイルでは、Asia/Tokyo は1887年12月31日 15:00(UTC)まで、東京の地方平均時(+9:18:59)とされています。それより前の日時では、+09:00 にはなりません。秒の部分は、この時差の表示には出していません。
夏時間の切り替えでは、入れた日時と Unix時間が1対1にならない
tz データベースでは、Asia/Tokyo の夏時間は1948年から1951年までで、いまの日本には夏時間がありません。ただ、利用者のブラウザのタイムゾーンが、夏時間のある地域になっていることはあります。Playwright でタイムゾーンを America/New_York にして試しました。
2026年3月8日は夏時間が始まる日で、01:59:59 の時差は -05:00、03:30 の時差は -04:00 でした。日時の欄に 02:30 を入れると、Unix時間は 1772955000 になり、03:30 を入れたときと同じ値でした。
| 日時の欄 | Unix時間 | UTC の行 |
|---|---|---|
| 2026-03-08T01:59:59 | 1772953199 | 2026-03-08T06:59:59.000Z(時差 -05:00) |
| 2026-03-08T02:30 | 1772955000 | 2026-03-08T07:30:00.000Z(時差 -04:00) |
| 2026-03-08T03:30 | 1772955000 | 2026-03-08T07:30:00.000Z(時差 -04:00) |
2026年11月1日は逆に、1時台が2回あります。日時の欄に 01:30 を入れると、1回目(-04:00)の 1793511000 になりました。2回目(-05:00)の 01:30 は、Unix時間の欄に 1793514600 と打てば日時の欄に 01:30 と出ますが、このコンポーネントでは、日時の欄から入れる方法はありません。
| Unix時間の欄 | 日時の欄 | UTC の行 |
|---|---|---|
| 1793511000 | 2026-11-01T01:30 | 2026-11-01T05:30:00.000Z(時差 -04:00) |
| 1793514600 | 2026-11-01T01:30 | 2026-11-01T06:30:00.000Z(時差 -05:00) |
| 1793518200 | 2026-11-01T02:30 | 2026-11-01T07:30:00.000Z(時差 -05:00) |
どちらも、MDN の Date の説明に書かれた動きのとおりです。現地時間が2つの瞬間に当たるときは早いほうが選ばれ、存在しない時刻は、ずれの分だけ先に進みます。MDN の datetime-local の説明にも、この欄は夏時間の切り替えの1時間のような、その地域では不正な組み合わせも入力できる、とあります。
Node.js で、1970年から2100年までの1時間ごとの Unix時間(1,139,569個)を日時にし、また Unix時間に戻してみました。Asia/Tokyo ではすべて元に戻りました。America/New_York では130個が元に戻らず、どれも、日時が1時台で、戻した値が1時間前になるものでした。秋の切り替えで2回目の1時台に当たる値です。
日時の欄だけを見ていると、この違いに気づけません。コンポーネントの下に UTC と時差を出しておくと、02:30 を入れたのに時差が -04:00 になっていることや、同じ 01:30 でも時差が違うことが、画面で分かります。





