時間の操作TimeTransportExperimental
いま映している値を、名前をつけた速さの段で動かし、決まった幅で飛ばし、ひと押しで「いま」へ戻すための操作盤です。時計は持たず、進めるのは呼び出し側です。
プレビュー
この見本は書き換えて試せます(速さの段と飛ばし幅が入れ替わります)
12:00:00
2026-09-12 JST
状態とバリエーション
実時間
value が now と同じとき。札が success の色と「実時間」の文字で出て、「いまへ戻る」は押せません(もう戻っているため)。
12:00:00
2026-09-12 JST
いまから外れている
value が now から離れているとき。札が warning の色と文字に変わり、「いまへ戻る」が押せるようになります。大きい表示の色は変えていません(下の「設計の判断」)。
07:00:00
2026-09-12 JST
再生中
再生ボタンが primary で押された状態(aria-pressed="true")になり、一時停止のアイコンに替わります。
10:00:00
2026-09-12 JST
飛ばしなし
jumps を渡さなければ飛ばしボタンの列は出ません。速さと再生だけの、いちばん小さい形です。
12:00:00
2026-09-12 JST
操作できない
全体が無効。読み取り専用の盤や、まだ再生するものが決まっていないときに使います。
12:00:00
2026-09-12 JST
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| value | number | - | いまの位置(エポックミリ秒・フレーム番号・刻みの数)。 |
| onValueChange | (next: number) => void | - | 位置が動いたときの、次の位置。 |
| now | number | - | 「いま」の位置。 |
| live | boolean | - | 実時間かどうかを、now から導かずに直接指定します。 |
| liveTolerance | number | - | now からどれだけ離れるまでを実時間とみなすか。 |
| onReturnToNow | () => void | - | 「いまへ戻る」を押したとき。既定は onValueChange(now) です。 |
| playing | boolean | - | 再生中かどうか(制御される値)。 |
| onPlayingChange | (playing: boolean) => void | - | 再生・一時停止を押したとき。 |
| speeds | TimeTransportSpeed[] | - | 速さの段。{ value, label } の配列で、名前は呼び出し側がつけます。 |
| speed / onSpeedChange | number / (speed: number) => void | - | 選ばれている速さと、その変更。 |
| jumps | TimeTransportJump[] | - | 決まった幅の飛ばし。offset は符号つきで、負が戻る方向です。 |
| formatValue | (value: number) => ReactNode | - | 大きい表示の書式。既定は String(value) です。 |
| secondary | ReactNode | - | 表示の下の2行目(日付・タイムゾーン・経過など)。 |
| scrubber | ReactNode | - | 掴んで動かす面のスロット。表示と操作段のあいだに全幅で入ります。 |
| labels | TimeTransportLabels | - | 組み込みの文字列の差し替え。既定は英語です。 |
| disabled | boolean | - | 操作全体を無効にします。 |
使い方
"use client";
import * as React from "react";
import { TimeTransport } from "@gunjo/ui";
// 時計は TimeTransport の外。進めるのは呼び出し側です。
// サーバと最初の1フレームを合わせるため、時刻は effect の中で読みます。
const BASE = Date.UTC(2026, 8, 12, 3, 0, 0);
const CLOCK = new Intl.DateTimeFormat("en-GB", {
timeZone: "Asia/Tokyo",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
function formatClock(at) {
return CLOCK.format(new Date(at));
}
export function Replay() {
const [now, setNow] = React.useState(BASE);
const [value, setValue] = React.useState(BASE);
const [following, setFollowing] = React.useState(true);
const [playing, setPlaying] = React.useState(false);
const [speed, setSpeed] = React.useState(60);
React.useEffect(() => {
let last = Date.now();
const id = window.setInterval(() => {
const t = Date.now();
const elapsed = t - last;
last = t;
setNow(t);
setValue((v) => (playing ? v + elapsed * speed : following ? t : v));
}, 200);
return () => window.clearInterval(id);
}, [playing, speed, following]);
return (
<TimeTransport
value={value}
now={now}
onValueChange={(next) => { setFollowing(false); setValue(next); }}
onReturnToNow={() => { setFollowing(true); setPlaying(false); setValue(now); }}
playing={playing}
onPlayingChange={setPlaying}
// 速さの段には呼び出し側が名前をつけます。1× が1秒なのか1日なのかは
// 部品には分かりません。
speeds={[
{ value: 1, label: "1×" },
{ value: 60, label: "1分/秒" },
{ value: 3600, label: "1時間/秒" },
{ value: 86400, label: "1日/秒" },
]}
speed={speed}
onSpeedChange={setSpeed}
// offset は符号つき。向きはアイコンが示すので、ラベルは大きさだけ。
jumps={[
{ offset: -86400000, label: "1日" },
{ offset: -3600000, label: "1時間" },
{ offset: 3600000, label: "1時間" },
{ offset: 86400000, label: "1日" },
]}
formatValue={(v) => formatClock(v)}
secondary="2026-09-12 JST"
/>
);
}設計の判断
- 速さは連続値ではなく、名前のついた段。出どころの実装は 1× から 10⁷× の対数の連続値でしたが、それは天文のアプリ固有の刻みです。 部品は
speeds[]で受け取ります。1× が1秒なのか1日なのかは、部品には分かりません。 - 大きい表示の色を、実時間かどうかで変えていません。意味を示す3色は、カードの背景に文字色として置くとコントラストの基準に届きません(未解決の課題として 記録済み)。状態は札が文字で言っているので、色に頼る必要がありません。
- 向きはアイコンで、ラベルは大きさだけ。
offsetの符号で左右に振り分け、向きはアイコンが示します。 ラベルに向きを書かせないので、そのまま多言語に出せます。 - 渡さなかったものは、出しません。
speedsが無ければ速さの選択、onPlayingChangeが無ければ再生ボタン、jumpsが無ければ飛ばしの列、nowもliveも無ければ 実時間の札と「いまへ戻る」が、それぞれ描かれません。 推測して間違った状態を見せるより、黙っているほうが正しいためです。 - 触れるところは 44px を割りません。再生・飛ばし・いまへ戻るは、いずれも高さ 44px です。
使用コンポーネント
関連コンポーネント
- Slider
値は選べるが、再生も速さも「いまへ戻る」も無い。
- TimePicker
Calendar と同じく絶対の時刻を選ぶもの。相対に動かせない。
- Stringline
Gantt と同じく時間を見せるだけ。操作は受け取らない。
- Timeline
出来事を並べて見せるだけ。動かす操作は持たない。
- SegmentedControl
速さの段だけなら、これ単体で足ります。
いつ・なぜ使うか(UIXHERO)
「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。