Lightweight timeline editor web component
Zero dependencies. Works everywhere.
Installation • Usage • API • Events • Styling • Demo
A timeline editor for building animation tools, video editors, audio sequencers, or anything that needs time-based sequencing. Built as a native Web Component with zero dependencies.
npm install @andymcloid/trakkOr use directly via CDN:
<script type="module" src="https://unpkg.com/@andymcloid/trakk/dist/trakk.esm.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@andymcloid/trakk/dist/trakk.css"><trakk-editor id="timeline"></trakk-editor>
<script type="module">
import { Trakk } from '@andymcloid/trakk';
import '@andymcloid/trakk/css';
const timeline = document.getElementById('timeline');
timeline.setData([
{
id: 'track-1',
name: 'Audio',
blocks: [
{ id: 'block-1', name: 'Intro', start: 0, end: 5 },
{ id: 'block-2', name: 'Main', start: 6, end: 15 }
]
},
{
id: 'track-2',
name: 'Video',
blocks: [
{ id: 'block-3', name: 'Scene 1', start: 0, end: 10 }
]
}
]);
// Play/pause
timeline.play({ autoEnd: true });
timeline.pause();
// Listen for changes
timeline.addEventListener('change', (e) => {
console.log('Updated:', e.detail.tracks);
});
</script>| Method | Description |
|---|---|
setData(tracks) |
Set timeline data |
play(options?) |
Start playback. Options: { autoEnd: boolean, toTime: number } |
pause() |
Pause playback |
setTime(time) |
Set current time position |
getTime() |
Get current time |
getTotalTime() |
Get total duration (end of last block) |
setConfig(config) |
Update configuration |
selectAction(action, row?) |
Select a block programmatically |
deselectAction() |
Clear the current selection |
getSelectedAction() |
Get currently selected block { action, row } or null |
saveToLocalStorage(key?) |
Save to localStorage |
loadFromLocalStorage(key?) |
Load from localStorage |
timeline.setConfig({
scale: 1, // Seconds per scale unit
scaleWidth: 160, // Pixels per scale unit
scaleCount: 20, // Number of scale units
startLeft: 100, // Left margin (label column width)
rowHeight: 32, // Track height in pixels
autoScroll: true, // Auto-scroll during playback
hideCursor: false, // Hide playhead cursor
disableDrag: false, // Disable all dragging
allowOverlap: false // Allow blocks to overlap (default: false)
});interface Track {
id: string;
name?: string;
locked?: boolean; // Prevent editing
blocks: Block[];
}
interface Block {
id: string;
name?: string;
start: number; // Start time in seconds
end: number; // End time in seconds
locked?: boolean; // Prevent editing this block
}// Data changed (drag, resize, create, delete)
timeline.addEventListener('change', (e) => {
console.log(e.detail.tracks);
});
// New block created via drag
timeline.addEventListener('itemcreated', (e) => {
console.log(e.detail.item, e.detail.row);
});
// Block deleted
timeline.addEventListener('blockdeleted', (e) => {
console.log(e.detail.block, e.detail.track);
});
// Track deleted
timeline.addEventListener('trackdeleted', (e) => {
console.log(e.detail.track);
});
// Block selected/deselected
timeline.addEventListener('select', (e) => {
if (e.detail) {
console.log('Selected:', e.detail.action, 'in track:', e.detail.row);
} else {
console.log('Selection cleared');
}
});
// Track/block renamed (double-click to edit)
timeline.addEventListener('trackrenamed', (e) => {
console.log(e.detail.track, e.detail.name);
});
timeline.addEventListener('blockrenamed', (e) => {
console.log(e.detail.block, e.detail.name);
});Access the playback engine directly:
timeline.engine.on('play', () => console.log('Playing'));
timeline.engine.on('paused', () => console.log('Paused'));
timeline.engine.on('ended', () => console.log('Ended'));
timeline.engine.on('setTimeByTick', ({ time }) => {
// Called every frame during playback
console.log('Current time:', time);
});For advanced control over interactions:
timeline.setCallbacks({
// Custom rendering
getActionRender: (block, track) => `<b>${block.name}</b>`,
getScaleRender: (time) => `${time.toFixed(1)}s`,
// Interaction hooks (return false to cancel)
onActionMoving: ({ action, start, end }) => {
if (start < 0) return false; // Prevent moving before 0
},
onActionResizing: ({ action, start, end }) => {
if (end - start < 0.5) return false; // Minimum 0.5s duration
},
// Click handlers (button: 0=left, 1=middle, 2=right)
onClickAction: (e, { action, row, time, button }) => {},
onDoubleClickAction: (e, { action, row, time }) => {},
onContextMenuAction: (e, { action, row, time, button }) => {
// Right-click on block - show custom context menu
},
onClickRow: (e, { row, time }) => {},
onClickTimeArea: (e, { time }) => {}
});Customize Trakk's appearance using CSS custom properties:
trakk-editor {
/* Core colors */
--trakk-bg: #191b1d; /* Background color */
--trakk-text: #ffffff; /* Text color */
--trakk-accent: #5297FF; /* Accent color (cursor, selection border) */
/* Block colors */
--trakk-block-bg: #2f3134; /* Block background */
--trakk-block-bg-hover: #3a3d40; /* Block hover state */
--trakk-block-bg-selected: #4a7ba7; /* Selected block background */
/* Borders */
--trakk-border: rgba(255, 255, 255, 0.1); /* Subtle borders */
--trakk-border-strong: rgba(255, 255, 255, 0.3); /* Prominent borders */
/* Text variations */
--trakk-text-muted: rgba(255, 255, 255, 0.6); /* Secondary text */
--trakk-text-subtle: rgba(255, 255, 255, 0.4); /* Subtle text/icons */
/* State */
--trakk-danger: #ff6464; /* Delete button hover */
--trakk-disabled-opacity: 0.6; /* Disabled elements */
--trakk-locked-opacity: 0.7; /* Locked elements */
}trakk-editor.light {
--trakk-bg: #f5f5f5;
--trakk-text: #1a1a1a;
--trakk-accent: #0066cc;
--trakk-block-bg: #e0e0e0;
--trakk-block-bg-hover: #d0d0d0;
--trakk-block-bg-selected: #b3d4fc;
--trakk-border: rgba(0, 0, 0, 0.1);
--trakk-border-strong: rgba(0, 0, 0, 0.2);
--trakk-text-muted: rgba(0, 0, 0, 0.6);
--trakk-text-subtle: rgba(0, 0, 0, 0.4);
}Open demo.html in a browser or check out the live demo.
MIT
