Start
//

This guide walks through using ef-timeline-ruler to display time markers in timeline editors. Each step builds on the previous one.

Step 1: Basic Timeline Ruler

Set duration-ms, fps, and content-width to render a ruler.

<div class="h-[32px] bg-gray-900 rounded overflow-hidden relative">
<ef-timeline-ruler
duration-ms="10000"
fps="30"
content-width="600"
></ef-timeline-ruler>
</div>

The ruler displays time markers at intervals that fit cleanly within the available space. Labels format as seconds ("0s", "1s", "2s") and adapt based on duration and zoom.

  • duration-ms — total timeline duration in milliseconds
  • content-width — total pixel width of the timeline content (controls density)
  • fps — frame rate used to calculate frame marker positions

Step 2: Scroll Synchronization

Synchronize the ruler with a scrollable timeline container using scroll-container-selector.

<div>
<div class="h-[32px] bg-gray-900 rounded-t overflow-hidden relative">
<ef-timeline-ruler
duration-ms="20000"
fps="30"
content-width="2000"
scroll-container-selector="#ruler-scroll-demo"
></ef-timeline-ruler>
</div>
<div id="ruler-scroll-demo" class="overflow-x-auto h-[80px] bg-gray-800 rounded-b">
<div class="h-full flex items-center" style="width:2000px;">
<div class="flex gap-4 px-4">
<div class="w-[200px] h-[50px] bg-blue-600 rounded flex items-center justify-center text-white text-sm">Clip A</div>
<div class="w-[300px] h-[50px] bg-purple-600 rounded flex items-center justify-center text-white text-sm">Clip B</div>
<div class="w-[150px] h-[50px] bg-green-600 rounded flex items-center justify-center text-white text-sm">Clip C</div>
</div>
</div>
</div>
</div>

Scroll the content area. The ruler markers stay aligned with the content as you scroll.

The ruler listens for scroll events on the target container and offsets its rendering to match. The content-width should equal the actual pixel width of the scrollable content.

Step 3: Frame Markers at High Zoom

Frame markers appear automatically when each frame is wide enough to display (≥ 5px per frame by default).

<div class="space-y-4">
<div>
<p class="text-gray-400 text-sm mb-1">Low zoom — time labels only:</p>
<div class="h-[32px] bg-gray-900 rounded overflow-hidden">
<ef-timeline-ruler
duration-ms="5000"
fps="30"
content-width="200"
></ef-timeline-ruler>
</div>
</div>
<div>
<p class="text-gray-400 text-sm mb-1">High zoom — frame markers visible:</p>
<div class="h-[32px] bg-gray-900 rounded overflow-auto">
<ef-timeline-ruler
duration-ms="5000"
fps="30"
content-width="3000"
></ef-timeline-ruler>
</div>
</div>
</div>

At high zoom the ruler switches to frame-level granularity. Frame markers are rendered lighter than time markers to indicate the two-tier hierarchy.

At 30fps over 5000ms there are 150 frames. At content-width="3000" each frame is 20px wide — well above the 5px threshold.

Step 4: Combining Zoom and Scroll

A practical timeline editor combines scrollable content with zoom-proportional content-width.

<div>
<div class="h-[32px] bg-gray-900 rounded-t overflow-hidden relative">
<ef-timeline-ruler
id="zoom-ruler"
duration-ms="10000"
fps="30"
content-width="1000"
scroll-container-selector="#zoom-scroll"
></ef-timeline-ruler>
</div>
<div id="zoom-scroll" class="overflow-x-auto bg-gray-800 rounded-b" style="height:80px;">
<div id="zoom-content" class="h-full flex items-center px-4 gap-3" style="width:1000px;">
<div class="w-[400px] h-[50px] bg-blue-600 rounded flex items-center justify-center text-white text-sm shrink-0">4s clip</div>
<div class="w-[300px] h-[50px] bg-purple-600 rounded flex items-center justify-center text-white text-sm shrink-0">3s clip</div>
<div class="w-[300px] h-[50px] bg-green-600 rounded flex items-center justify-center text-white text-sm shrink-0">3s clip</div>
</div>
</div>
<div class="flex gap-2 mt-3">
<button id="zoom-in-btn" class="px-3 py-1 bg-blue-600 text-white rounded text-sm">Zoom In</button>
<button id="zoom-out-btn" class="px-3 py-1 bg-gray-600 text-white rounded text-sm">Zoom Out</button>
</div>
</div>
<script>
let zoom = 1;
const BASE_WIDTH = 1000;
const ruler = document.getElementById('zoom-ruler');
const content = document.getElementById('zoom-content');
function applyZoom() {
const w = Math.round(BASE_WIDTH * zoom);
ruler.setAttribute('content-width', w);
content.style.width = w + 'px';
// Scale clip widths proportionally
const clips = content.querySelectorAll('div');
const clipWidths = [400, 300, 300];
clips.forEach((c, i) => { c.style.width = Math.round(clipWidths[i] * zoom) + 'px'; });
}
document.getElementById('zoom-in-btn').addEventListener('click', () => { zoom = Math.min(zoom * 1.5, 20); applyZoom(); });
document.getElementById('zoom-out-btn').addEventListener('click', () => { zoom = Math.max(zoom / 1.5, 0.5); applyZoom(); });
</script>

As zoom increases, content-width grows proportionally. The ruler automatically switches between time-label and frame-marker display based on the resulting pixel-per-frame density.

How Time Mapping Works

The ruler maps time to horizontal position using a simple linear relationship:

pixelsPerMs = contentWidth / durationMs
xPosition = timeMs * pixelsPerMs

The content-width attribute encodes the zoom level. A wider content-width for the same duration-ms means more pixels per millisecond — higher zoom.

To convert a scroll offset back to a time position:

const timeMs = scrollLeft / pixelsPerMs;
// where pixelsPerMs = contentWidth / durationMs

How Tick Density is Calculated

The ruler chooses tick intervals from a preferred set so ticks never overlap. The algorithm:

  1. Calculate pixelsPerMs = contentWidth / durationMs
  2. Try intervals in order: 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 30s, 60s
  3. Select the smallest interval where interval * pixelsPerMs >= minLabelSpacing (typically ~60px)
  4. If pixelsPerFrame >= 5, also render frame-level ticks between time ticks

This ensures the displayed interval is always a "clean" number regardless of zoom level, and ticks are never closer than the minimum spacing.

Using the Helper Functions

The module exports utility functions for frame-accurate calculations:

import {
quantizeToFrameTimeMs,
calculateFrameIntervalMs,
calculatePixelsPerFrame,
shouldShowFrameMarkers
} from '@editframe/elements';
// Snap a time value to the nearest frame boundary
const snapped = quantizeToFrameTimeMs(1234, 30); // → 1233ms (frame 37 at 30fps)
// Duration of one frame
const frameDuration = calculateFrameIntervalMs(30); // → 33.333ms
// Pixel width of one frame at current zoom
const pixelsPerMs = 1000 / 10000; // contentWidth / durationMs
const frameWidth = calculatePixelsPerFrame(33.333, pixelsPerMs); // → 3.33px
// Whether frame markers should be shown
const show = shouldShowFrameMarkers(3.33); // → false (below 5px threshold)
const show2 = shouldShowFrameMarkers(8.0); // → true

quantizeToFrameTimeMs is particularly useful when implementing a playhead scrubber — snap the playhead to the nearest frame as the user drags.

Virtualized Rendering

The ruler uses canvas-based virtualized rendering for performance on long timelines:

  • Only the visible region (plus a 200px buffer on each side) is drawn
  • Maximum canvas width is capped at 2000px regardless of content-width
  • Scroll position is applied via canvas offset, not by moving the canvas element

This means a 10-minute timeline (duration-ms="600000") at high zoom renders identically to a short one — performance does not degrade with duration.