Editor Toolkit
ef-timeline-ruler Tutorial
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-rulerduration-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 millisecondscontent-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-rulerduration-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-rulerduration-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-rulerduration-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-rulerid="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 proportionallyconst 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 / durationMsxPosition = 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:
- Calculate
pixelsPerMs = contentWidth / durationMs - Try intervals in order: 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 30s, 60s
- Select the smallest interval where
interval * pixelsPerMs >= minLabelSpacing(typically ~60px) - 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 boundaryconst snapped = quantizeToFrameTimeMs(1234, 30); // → 1233ms (frame 37 at 30fps)// Duration of one frameconst frameDuration = calculateFrameIntervalMs(30); // → 33.333ms// Pixel width of one frame at current zoomconst pixelsPerMs = 1000 / 10000; // contentWidth / durationMsconst frameWidth = calculatePixelsPerFrame(33.333, pixelsPerMs); // → 3.33px// Whether frame markers should be shownconst 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.