TimeRuler
5 partsA headless, zoomable time axis: a horizontal ruler of ticks and labels over a
span of duration seconds. It renders the accessible group (region when
labelled), measures its own width, and builds a useScale whose domain is the
visible time window [offset, offset + width / zoom] and range [0, width].
The visible window is driven by two models — offset (the left-edge time in
seconds, v-model:offset) and zoom (pixels-per-second, v-model:zoom) —
which stream continuously while panning / zooming; the root additionally emits
SETTLE events (panCommit, zoomCommit, rangeChange) when a gesture ends.
Tick generation is selected by mode: 'seconds' uses the human time ladder,
'timecode' renders HH:MM:SS:FF SMPTE labels at fps (drop-frame optional),
and 'frames' renders integer frame numbers. When focusable the root handles
a keyboard layer (Arrow keys pan, Shift+Arrow pans by a major interval, + /
- zoom about the canvas centre) and optional wheel / drag panning.
It is usable standalone or embedded in a Timeline via TimeRulerContext,
which exposes the tick collections, the scale / invert projectors, the
offset / zoom models, and the formatTime helper. The default slot
surfaces { ticks, majorTicks, minorTicks, scale, formatTime } so consumers
can render their own tick layer; the TimeRuler* parts are opt-in.
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
TimeRulerRoot,
TimeRulerTick,
TimeRulerLabel,
TimeRulerCursor,
TimeRulerScreenReaderSummary,
} from '@robonen/primitives/canvas/time-ruler';
<TimeRulerRoot>
<TimeRulerTick />
<TimeRulerLabel />
<TimeRulerCursor />
<TimeRulerScreenReaderSummary />
</TimeRulerRoot>API Reference
TimeRulerRoot
RootA headless, zoomable time axis: a horizontal ruler of ticks and labels over a span of `duration` seconds. It renders the accessible `group` (region when labelled), measures its own width, and builds a `useScale` whose domain is the visible time window `[offset, offset + width / zoom]` and range `[0, width]`. The visible window is driven by two models — `offset` (the left-edge time in seconds, `v-model:offset`) and `zoom` (pixels-per-second, `v-model:zoom`) — which stream continuously while panning / zooming; the root additionally emits SETTLE events (`panCommit`, `zoomCommit`, `rangeChange`) when a gesture ends. Tick generation is selected by `mode`: `'seconds'` uses the human time ladder, `'timecode'` renders `HH:MM:SS:FF` SMPTE labels at `fps` (drop-frame optional), and `'frames'` renders integer frame numbers. When focusable the root handles a keyboard layer (Arrow keys pan, Shift+Arrow pans by a major interval, `+` / `-` zoom about the canvas centre) and optional wheel / drag panning. It is usable standalone or embedded in a `Timeline` via `TimeRulerContext`, which exposes the tick collections, the `scale` / `invert` projectors, the `offset` / `zoom` models, and the `formatTime` helper. The default slot surfaces `{ ticks, majorTicks, minorTicks, scale, formatTime }` so consumers can render their own tick layer; the `TimeRuler*` parts are opt-in.
| Prop | Type | Description |
|---|---|---|
duration? | number | Total content duration in seconds. |
fps? | number | Frame rate used by 'timecode' / 'frames' modes. |
mode? | TimeRulerMode | How tick labels are rendered. |
dropFrame? | boolean | Use drop-frame timecode labels (only meaningful in 'timecode' mode). |
minZoom? | number | Minimum zoom in pixels-per-second; zoom is clamped to this. |
maxZoom? | number | Maximum zoom in pixels-per-second; zoom is clamped to this. |
disabled? | boolean | Disable all interaction (keyboard, wheel, drag). |
dir? | TimeRulerDirection | Writing direction. When omitted it is inherited from the nearest
ConfigProvider (falling back to 'ltr'); an explicit value wins. |
targetDensity? | number | Target pixel spacing between adjacent ticks, forwarded to the tick generator. |
focusable? | boolean | Make the ruler focusable and enable the keyboard layer. |
wheel? | boolean | Enable wheel panning (and ctrl/cmd+wheel zoom about the pointer). |
draggable? | boolean | Enable drag-to-pan (x-only). |
offset? | number | Two-way bound value (v-model:offset). |
zoom? | number | Two-way bound value (v-model:zoom). |
| Event | Payload |
|---|---|
update:offset | [value: number] |
update:zoom | [value: number] |
TimeRulerTick
TickA single gridline of the ruler. Consumers usually render their own tick layer by looping the `ticks` exposed on `TimeRulerRoot`'s default slot, but this part provides a ready-made, positioned element for the common case. It is `aria-hidden` by default (the screen-reader summary announces the window instead of every tick), reflects `tick.major` on `data-major`, and positions itself at `tick.px` via an inline `left` (overridable through merged styles).
| Prop | Type | Description |
|---|---|---|
tick | Tick | The tick to render (from TimeRulerRoot's exposed ticks). |
TimeRulerLabel
LabelThe text label for a tick (`tick.label`). Rendered for `major` ticks in the default tickers; `aria-hidden` so the visual labels do not duplicate the screen-reader summary. Positioned at `tick.px` like `TimeRulerTick`; the default slot falls back to `tick.label` so it works without a slot.
| Prop | Type | Description |
|---|---|---|
tick | Tick | The tick whose label to render. |
TimeRulerCursor
CursorAn optional playhead / hover indicator, positioned at a given `time` (seconds) projected through the ruler's `scale`. Purely presentational (`role` defaults to `'presentation'` and it is `aria-hidden`) — it conveys no semantics of its own; the playhead time should be announced by the host (e.g. a transport control), not the ruler. When the projected pixel falls outside the visible width the cursor is hidden (rendered with `visibility: hidden`) so an off-screen playhead does not bleed past the ruler edges; the element stays in the tree for stable transitions.
| Prop | Type | Description |
|---|---|---|
time | number | Playhead / hover time in seconds. |
TimeRulerScreenReaderSummary
ScreenReaderSummaryA visually-hidden, `aria-live="polite"` region that announces the visible window after it settles — the accessible alternative to announcing every tick. It debounces on every `offset` / `zoom` / `mode` / `fps` change and, once the viewport stops moving for `settleDelay` ms, writes a single human summary: the visible time range, the seconds-per-tick (zoom granularity), and the frame rate (in frame-based modes). Provide `formatMessage` to fully customise the announcement.
| Prop | Type | Description |
|---|---|---|
settleDelay? | number | Debounce before announcing, in milliseconds. |
formatMessage? | (view: {
start: number;
end: number;
secondsPerTick: number;
zoom: number;
fps: number;
mode: string;
formatTime: (seconds: number) => string;
}) => string | Build the announcement string. Receives the settled view (visible start /
end seconds, secondsPerTick, zoom, fps, mode) and the bound
formatTime helper. Defaults to a "Showing <a> to <b>, …" sentence. |