A canvas-based interactive timeline component for Svelte 5 with zoom, pan, scrub, and pluggable rendering layers.
Built for media playback interfaces, data visualization tools, and anywhere you need a high-performance timeline with rich interactions.
npm install svelte-interactive-timeline
# or
yarn add svelte-interactive-timeline
Requires Svelte 5 as a peer dependency.
<script lang="ts">
import { onMount } from 'svelte';
import { TimelineContainer, timelineStore } from 'svelte-interactive-timeline';
import type { TimelineCallbacks, PlaybackState, PlaybackCallbacks } from 'svelte-interactive-timeline';
let isPlaying = $state(false);
let speedIndex = $state(1);
const SPEED_PRESETS = [
{ value: 0.5, label: '0.5x' },
{ value: 1, label: '1x' },
{ value: 2, label: '2x' },
{ value: 5, label: '5x' },
{ value: 10, label: '10x' },
];
let playback: PlaybackState = $derived({
isPlaying,
speedIndex,
speedPresets: SPEED_PRESETS,
});
const playbackCallbacks: PlaybackCallbacks = {
onTogglePlayback: () => (isPlaying = !isPlaying),
onPause: () => (isPlaying = false),
onSpeedChange: (idx) => (speedIndex = idx),
};
const callbacks: TimelineCallbacks = {
onSeek: (time) => console.log('Seeked to', time),
onViewChange: (start, end) => console.log('View changed', start, end),
};
onMount(() => {
timelineStore.initialize(120); // 2-minute timeline
});
</script>
<TimelineContainer
height={60}
showControls={true}
{callbacks}
{playback}
{playbackCallbacks}
/>
<TimelineContainer>The main component. Composes the canvas and controls.
| Prop | Type | Default | Description |
|---|---|---|---|
endTime |
number |
0 |
Initialize with this duration (alternative to calling timelineStore.initialize()) |
height |
number |
80 |
Canvas height in pixels |
showControls |
boolean |
true |
Show the playback/zoom control bar |
embedded |
boolean |
false |
Embedded mode — removes border/shadow |
customLayers |
RenderLayer[] |
[] |
Custom render layers (inserted between background and playhead) |
callbacks |
TimelineCallbacks |
{} |
Event callbacks for seek, view change, redraw |
playback |
PlaybackState |
— | Current playback state |
playbackCallbacks |
PlaybackCallbacks |
{} |
Playback control callbacks |
showGradientToggle |
boolean |
false |
Show an optional gradient toggle button |
gradientActive |
boolean |
false |
Gradient toggle state |
onGradientToggle |
() => void |
— | Gradient toggle callback |
Exported methods (via bind:this):
initialize(end, start?) — set data rangesetCurrentTime(time) — move playheadgetRenderer() — access the TimelineRenderer instance<TimelineCanvas>The canvas element with pointer/wheel event handling. Used internally by TimelineContainer, but can be used standalone for custom layouts.
<TimelineControls>The control bar (play/pause, speed, zoom buttons, time display). Also used internally but available for custom layouts.
import { timelineStore, createTimelineStore } from 'svelte-interactive-timeline';
timelineStore is a default singleton. Use createTimelineStore() if you need multiple independent timelines.
| Method | Description |
|---|---|
initialize(endTime, startTime?) |
Set data time range |
reset() |
Reset to empty state |
setCurrentTime(time) |
Move playhead (clamped to data range) |
setView(start, end) |
Set visible window |
zoom(factor, centerTime?) |
Zoom in (< 1) or out (> 1) around a point |
zoomToFit() |
Reset view to show full data range |
pan(deltaTime) |
Shift view by time offset |
getState() |
Get current TimelineState snapshot |
hasData() |
Whether timeline has been initialized |
import { viewDuration, dataDuration, zoomLevel, isZoomed } from 'svelte-interactive-timeline';
viewDuration — current visible time spandataDuration — total data time spanzoomLevel — ratio of data to view duration (1 = no zoom)isZoomed — whether the timeline is zoomed inAdd your own visualizations by implementing the RenderLayer interface:
import type { RenderLayer, RenderContext } from 'svelte-interactive-timeline';
const myLayer: RenderLayer = {
name: 'my-overlay',
visible: true,
render({ ctx, state, width, height, timeToX }: RenderContext) {
// Draw a marker at time = 30s
const x = timeToX(30);
ctx.fillStyle = 'rgba(59, 130, 246, 0.5)';
ctx.fillRect(x - 1, 0, 2, height);
},
};
Pass custom layers via the customLayers prop — they render between the background grid and the playhead.
You can also add/remove layers dynamically via the renderer:
const renderer = containerRef.getRenderer();
renderer.addLayer(myLayer); // inserted before playhead
renderer.removeLayer('my-overlay');
renderer.getLayer('my-overlay');
Every layer's render() receives:
| Field | Type | Description |
|---|---|---|
ctx |
CanvasRenderingContext2D |
The canvas context (already DPI-scaled) |
state |
TimelineState |
Full timeline state snapshot |
width |
number |
Canvas width in CSS pixels |
height |
number |
Canvas height in CSS pixels |
dpr |
number |
Device pixel ratio |
timeToX(time) |
(number) => number |
Convert time to X pixel (respects zoom/pan) |
xToTime(x) |
(number) => number |
Convert X pixel to time (respects zoom/pan) |
All types are exported:
import type {
TimelineState,
RenderContext,
RenderLayer,
DragTarget,
HitTarget,
TimelineCallbacks,
PlaybackState,
PlaybackCallbacks,
} from 'svelte-interactive-timeline';
import {
formatTime, // (seconds, format?) => string
clamp, // (value, min, max) => number
mapRange, // (value, inStart, inEnd, outStart, outEnd) => number
generateGridLines, // (viewStart, viewEnd, maxLabels?) => GridLine[]
calculateGridInterval,
getDevicePixelRatio,
resetShadow,
} from 'svelte-interactive-timeline';
Layout and color constants are exported for custom layer authors:
import { MONO_FONT, PLAYHEAD_HEAD_HEIGHT, LABEL_TOP_OFFSET } from 'svelte-interactive-timeline';
import { ACCENT, ACCENT_LIGHT, GRID_MAJOR, GRID_MINOR } from 'svelte-interactive-timeline';
| Action | Effect |
|---|---|
| Click on track | Seek to position |
| Drag playhead | Scrub |
| Drag on track | Drag-to-zoom selection |
| Ctrl/Cmd + scroll | Zoom at cursor |
| Scroll (when zoomed) | Pan |
| Alt + drag | Pan |
| Middle-click + drag | Pan |
| Zoom +/- buttons | Step zoom in/out |
| Fit button | Reset to full view |
| Speed label click | Cycle speed presets (Shift+click for slower) |
git clone https://github.com/benshapiro/svelte-interactive-timeline.git
cd svelte-interactive-timeline
yarn install
yarn dev # dev server with demo page
yarn check # type checking
yarn package # build distributable to dist/