時間の操作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
実時間

プロパティ

表は横にスクロールできます
プロパティ型初期値説明
valuenumber-いまの位置(エポックミリ秒・フレーム番号・刻みの数)。
onValueChange(next: number) => void-位置が動いたときの、次の位置。
nownumber-「いま」の位置。
liveboolean-実時間かどうかを、now から導かずに直接指定します。
liveTolerancenumber-now からどれだけ離れるまでを実時間とみなすか。
onReturnToNow() => void-「いまへ戻る」を押したとき。既定は onValueChange(now) です。
playingboolean-再生中かどうか(制御される値)。
onPlayingChange(playing: boolean) => void-再生・一時停止を押したとき。
speedsTimeTransportSpeed[]-速さの段。{ value, label } の配列で、名前は呼び出し側がつけます。
speed / onSpeedChangenumber / (speed: number) => void-選ばれている速さと、その変更。
jumpsTimeTransportJump[]-決まった幅の飛ばし。offset は符号つきで、負が戻る方向です。
formatValue(value: number) => ReactNode-大きい表示の書式。既定は String(value) です。
secondaryReactNode-表示の下の2行目(日付・タイムゾーン・経過など)。
scrubberReactNode-掴んで動かす面のスロット。表示と操作段のあいだに全幅で入ります。
labelsTimeTransportLabels-組み込みの文字列の差し替え。既定は英語です。
disabledboolean-操作全体を無効にします。

使い方

"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 の記事で解説しています。