いちにちの帯DayBandExperimental

1日を1本の帯にして、時間帯を面で、節目を印で、いまの時刻とつまみの位置を重ねて見せます。日の出も「いま」も呼び出し側が渡す数で、部品は1日について何も計算しません。

プレビュー

東京 9月12日14:30
日の出 05:22日の入 17:56いま
06121824

状態とバリエーション

TimeTransport の中に入れる

操作盤の scrubber スロットへ差し込んだ形。帯を掴めば時刻が動き、「いまへ戻る」で戻ります。この組み合わせのために作られた部品です。ここでの単位は「分」なので、TimeTransport の liveTolerance も分で渡しています(既定の 1000 はエポックミリ秒の1秒ぶんです)。

14:30
2026-09-12 JST
いまではない
日の出 05:22日の入 17:56いま
06121824

当番表

同じ形で当番表になります。時間帯にトーン名を渡すと、面のトークン(--*-subtle)へ解決します。節目の印は交代の時刻です。

3階病棟 9月12日14:30
交代 08:00交代 18:00
06121824

見せるだけ(sm)

onValueChange を渡さなければ操作を受け取らず、焦点も持ちません。value も外せば、つまみのない営業時間の帯になります。size="sm" は 24px です。

営業時間11:00 – 22:00
ラストオーダー 21:00
06121824

掴んで流す(relative)

触っただけでは動かず、掴んだ距離ぶんだけ動きます。細い帯で指が現在位置を隠さず、誤って触っても時刻が飛びません。0時をまたぐと帯の外の値も返るので、呼び出し側が日を入れ替えます。

14:30
日の出 05:22日の入 17:56いま
06121824

いちばん小さい形

時間帯だけ。印も時刻の数字も目盛りも外せます(marks を渡さず、hourLabels={[]}、tickEvery={0})。表の行の中など、狭いところに置くときの形です。

操作できない

帯全体が無効。読み取り専用の盤や、まだ日が決まっていないときに使います。

14:30
日の出 05:22日の入 17:56
06121824

プロパティ

表は横にスクロールできます
プロパティ型初期値説明
min / maxnumber / number-1日の始まりと終わり。既定は午前0時からの分です。
valuenumber-つまみが指す位置。
onValueChange(next: number) => void-掴んで動かしたときの、次の位置。
phasesDayBandPhase[]-1日の中の時間帯。{ start, end, label?, color? } が面として敷かれます。
marksDayBandMark[]-節目の印。{ at, label?, color? } で、ラベルは帯の上の行に出ます。
nownumber-「いま」の位置。それ自身の印として描かれます。
scrub"absolute" | "relative"-触った位置がそのまま時刻(absolute)か、掴んだ距離ぶん動く(relative)か。
step / coarseStepnumber / number-矢印キーの刻みと、Shift+矢印・Page キーの刻み。既定は15分と1時間です。
tickEverynumber-帯の中に立てる目盛りの間隔(時間)。0 で消えます。
hourLabels / formatHournumber[] / (hour: number) => ReactNode-帯の下に出す時刻の数字と、その書式。[] で行ごと消えます。
formatValue(value: number) => string-読み上げ(aria-valuetext)の書式。既定は「10:40 · 昼」です。
size"sm" | "default"-帯の高さ。default は触れる 44px、sm は 24px です。
label / hintReactNode / ReactNode-帯の上の行。左が label、右が hint です。
labelsDayBandLabels-組み込みの文字列の差し替え(band / now)。既定は英語です。
disabledboolean-帯を無効にし、タブ移動からも外します。

使い方

"use client";

import * as React from "react";
import { DayBand, TimeTransport } from "@gunjo/ui";

// 既定の単位は「午前0時からの分」(min 0 / max 1440)。
// 実日付で使うなら min にその日の0時のエポックミリ秒、max に +86400000 を渡します。
const PHASES = [
  { start: 0, end: 260, label: "夜", color: "hsl(var(--gunjo-deepest))" },
  { start: 260, end: 322, label: "薄明", color: "hsl(var(--gunjo-deep))" },
  { start: 322, end: 1076, label: "昼", color: "hsl(var(--gunjo-bright))" },
  { start: 1076, end: 1138, label: "薄明", color: "hsl(var(--gunjo-deep))" },
  { start: 1138, end: 1440, label: "夜", color: "hsl(var(--gunjo-deepest))" },
];

// 日の出・日の入は呼び出し側が渡します。部品は天文計算を持ちません。
const MARKS = [
  { at: 322, label: "日の出 05:22", color: "warning" },
  { at: 1076, label: "日の入 17:56", color: "info" },
];

function clock(minutes) {
  const hh = String(Math.floor(minutes / 60)).padStart(2, "0");
  const mm = String(minutes % 60).padStart(2, "0");
  return `${hh}:${mm}`;
}

export function SunBand() {
  const [value, setValue] = React.useState(640);
  // いまの時刻を描画中に読まないこと。effect の中で読みます。
  const [nowMinutes, setNowMinutes] = React.useState(undefined);

  React.useEffect(() => {
    const at = new Date();
    setNowMinutes(at.getHours() * 60 + at.getMinutes());
  }, []);

  return (
    <DayBand
      value={value}
      onValueChange={setValue}
      phases={PHASES}
      marks={MARKS}
      now={nowMinutes}
      label="東京 9月12日"
      hint={clock(value)}
      labels={{ band: "いちにちの中の時刻", now: "いま" }}
    />
  );
}

// TimeTransport の掴んで動かす面として差し込む場合
export function BandInTransport() {
  const [value, setValue] = React.useState(640);

  return (
    <TimeTransport
      value={value}
      now={640}
      onValueChange={setValue}
      liveTolerance={1}
      formatValue={clock}
      scrubber={
        <DayBand value={value} now={640} onValueChange={setValue} phases={PHASES} />
      }
    />
  );
}

設計の判断

  • 時間帯の色は「面のトークン」へ解決します。success のようなトーン名は、強い色ではなく --success-subtle の側へ 落とします。時間帯は下地で、その上に印とつまみが乗るからです。空のような絵としての色が要るときは、任意の CSS 色をそのまま渡せます。
  • つまみは2色で描いています。下地の時間帯は明色でも暗色でもありうるので、背景色の線に前景色の縁を重ねています。 片方の色に賭けると、夜の帯か昼の帯のどちらかで必ず消えます。
  • 印のラベルは帯の上、時刻の数字は帯の下。出どころの実装は1つの行に詰めていたため、印から48px以内の時刻の数字を消して衝突を避けていました。 行を分ければ、消す必要がありません。
  • 指で触ったときは、押した瞬間には動きません。押した指がそのまま縦スクロールに変わることがあるためで、動かしたときか、指を離したとき (=はっきりした「触った」)に確定します。マウスとペンは押した瞬間です。 帯の上から始めた縦スクロールも止めません(touch-action: pan-y)。
  • 色だけに意味を載せません。読み上げには「10:40 · 昼」のように時間帯の名前が乗り、節目は文字のラベルを 持ち、時間帯にはポインタで読める名前がつきます。矢印・Shift+矢印・PageUp / PageDown・ Home / End で、触らずに1日を歩けます。
  • 単位は1つだけ。min / max / phases / marks / value / now は、すべて同じ単位で書きます。既定は午前0時からの分 (0〜1440)で、実日付ならその日の0時のエポックミリ秒と +86400000 を渡します。 部品が単位を知らないので、混ぜると黙ってずれます。
  • 渡さなかったものは、出しません。value が無ければつまみが消えて見せるだけの帯になり、onValueChange が無ければ操作も焦点も受け取りません。now が無ければ「いま」の印も出ません。
  • 0時をまたぐ時間帯は2つに分けます。1本の帯は1日です。start が end 以上の時間帯は描かず、開発時に 警告を出します(黙って反対側を塗るより、言うほうが正しいためです)。

使用コンポーネント

  • TimeTransport

    時間を動かす操作盤。この帯はその scrubber に入る面です。

  • Slider

    RangeSlider と同じく値を選ぶだけ。1日の器も時間帯の面も無い。

  • SegmentTimelineCard

    任意の区間を段で見せるカード。こちらの器はちょうど1日。

  • WeekView

    ScheduleGrid と同じく日が複数のとき。1本の帯では足りない。

いつ・なぜ使うか(UIXHERO)

「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。