Timeline Ruler
A pure, zoom-aware, horizontally-scrollable time ruler: adaptive tick density and MM:SS / frame labels, with viewport virtualization. Just the scale — playhead, seeking, and tracks are left to the consumer.
A horizontal time ruler — the scale that sits beneath a video editor's
timeline tracks. Tick density adapts to the zoom level: zoomed out it
labels whole seconds as MM:SS (H:MM:SS past an hour); zoomed in it
subdivides each second and labels individual frames as Xf. Tick and
label intervals are chosen so labels stay readable (~120px apart) while
ticks pack denser (~18px), and every label always lands on a tick.
The ruler scrolls horizontally and virtualizes its ticks — only the marks inside the viewport (plus a small buffer) are rendered, so a long timeline stays cheap. It is fully self-contained: no editor store, no external state.
It is only the scale. A playhead, click-to-seek, and the video/audio tracks it sits above are application concerns — the consumer composes them on top of the ruler. The component renders ticks and labels, nothing more.
Installation
One-time setup: add the @ikui registry to your components.json.
{
"registries": {
"@ikui": "https://ik-ui.pages.dev/r/{name}.json"
}
}Then install the component:
pnpm dlx shadcn@latest add @ikui/timeline-rulerUsage
import { TimelineRuler } from "@/components/timeline-ruler";const [zoom, setZoom] = useState(1)
<TimelineRuler duration={120} zoom={zoom} fps={30} />zoom multiplies pixelsPerSecond, so the same component covers a fully
zoomed-out overview and a frame-accurate close-up. A playhead or
click-to-seek is the consumer's job: wrap the ruler, read the click x,
and divide by pixelsPerSecond * zoom to get the time.
Embedding above tracks
By default the ruler owns its own horizontal scrollbar. To place it above
timeline tracks that share one scrollbar — like a video editor — pass
the shared scroll container as scrollRef. The ruler then renders inline
(no scrollbar of its own) and virtualizes against that container, so it
scrolls in lockstep with the tracks below.
const scrollRef = useRef<HTMLDivElement>(null)
<div ref={scrollRef} className="overflow-x-auto">
<div style={{ width: duration * 50 * zoom }}>
<TimelineRuler duration={duration} zoom={zoom} scrollRef={scrollRef} />
<ThumbnailStrip /* video track at the same width */ />
{/* …audio track, etc. */}
</div>
</div>Examples
Zooming
zoom multiplies pixelsPerSecond, so one component spans a zoomed-out
overview and a frame-accurate close-up. The tick and label density adapt as
you zoom — whole seconds (MM:SS) out, individual frames (Xf) in.
In a timeline
Embedded above a video track (thumbnail-strip) and an audio waveform
(audio-waveform), all sharing one horizontal scrollbar through a
single scrollRef. The ruler scrolls in lockstep with the tracks beneath
it — this is the layout a video editor uses for its bottom scale. The
playhead, seeking, and tracks are all built in this demo's composition
layer, not by the ruler.
Props
| Prop | Type | Default |
|---|---|---|
duration | number | - |
pixelsPerSecond | number | 50 |
zoom | number | 1 |
fps | number | 30 |
height | number | 24 |
scrollRef | RefObject<HTMLElement | null> | - |
tickColor | string | "currentColor" |
className | string | - |
style | React.CSSProperties | - |