Command Palette

Search for a command to run...

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.

components.json
{
  "registries": {
    "@ikui": "https://ik-ui.pages.dev/r/{name}.json"
  }
}

Then install the component:

pnpm dlx shadcn@latest add @ikui/timeline-ruler

Usage

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

PropTypeDefault
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
-