Appearance
@vibe-labs/design-vue-timeline
Vue 3 components and composables for timeline-based editors. Wraps @vibe-labs/design-components-timeline CSS with behaviour, accessibility, provide/inject context, playback, drag/resize, selection, keyboard shortcuts, and zoom.
Zero CSS in this package. All visual styling comes from @vibe-labs/design-components-timeline.
Installation
bash
npm install @vibe-labs/design-vue-timelineRequires the component-level CSS to be imported separately:
css
@import "@vibe-labs/design-components-timeline";Quick Start
vue
<script setup lang="ts">
import { ref } from "vue";
import {
VibeTimeline,
VibeTimelineRuler,
VibeTimelineTrack,
VibeTimelineBlock,
VibeTimelineMarker,
VibeTimelinePlayhead,
VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";
const currentTime = ref(0);
const playing = ref(false);
</script>
<template>
<VibeTimeline
:duration="30"
:current-time="currentTime"
:playing="playing"
density="normal"
snap="quarter"
snapping
>
<VibeTimelineRuler :major-interval="1" :minor-subdivisions="4" />
<VibeTimelineTrack kind="dialogue" label="Bob">
<VibeTimelineGrid :major-interval="1" />
<VibeTimelineBlock
variant="speech"
:start="2"
:duration="3.5"
label="Hello there"
/>
<VibeTimelineMarker variant="cue" :time="5.5" label="Blink" />
</VibeTimelineTrack>
<VibeTimelineTrack kind="event" label="Camera">
<VibeTimelineGrid :major-interval="1" />
<VibeTimelineBlock
variant="gesture"
:start="1"
:duration="4"
label="Pan right"
/>
</VibeTimelineTrack>
<VibeTimelinePlayhead />
</VibeTimeline>
</template>Components
<VibeTimeline>
Root container. Sets up the timeline context via provide/inject. All child components read coordinate conversion, snap settings, and playback state from this context.
| Prop | Type | Default | Description |
|---|---|---|---|
duration | number | required | Total timeline duration (time units) |
currentTime | number | 0 | Playhead position |
density | TimelineZoomDensity | "normal" | Horizontal zoom |
snap | TimelineSnapResolution | "beat" | Grid snap resolution |
snapping | boolean | true | Whether snapping is active |
looping | boolean | false | Loop mode |
playing | boolean | false | Playback active |
pixelsPerUnit | number | 100 | Base px/unit before density scaling |
loopStart | number | — | Loop region start |
loopEnd | number | — | Loop region end |
headerWidth | string | — | Override sidebar width (CSS value) |
| Event | Payload | Description |
|---|---|---|
update:currentTime | number | Playhead moved |
update:playing | boolean | Playback toggled |
update:density | string | Zoom changed |
seek | number | Explicit seek |
| Slot | Scoped Props | Description |
|---|---|---|
| default | { timeline, contentWidth } | All child content |
Exposed: timeline (composable), el (root HTMLElement ref)
<VibeTimelineRuler>
Time axis along the top edge with tick marks.
| Prop | Type | Default | Description |
|---|---|---|---|
majorInterval | number | 1 | Major tick spacing (time units) |
minorSubdivisions | number | 4 | Minor ticks per major |
formatLabel | (time: number) => string | mm:ss.s | Custom tick label formatter |
<VibeTimelineTrackGroup>
Collapsible group wrapping multiple tracks.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Group name |
collapsed | boolean | false | Whether collapsed |
id | string | — | Unique identifier |
| Event | Payload |
|---|---|
update:collapsed | boolean |
toggle | — |
| Slot | Description |
|---|---|
label | Custom group header |
| default | Child tracks |
<VibeTimelineTrack>
Single horizontal lane with header sidebar.
| Prop | Type | Default | Description |
|---|---|---|---|
kind | TimelineTrackKind | "audio" | Track type (accent colour) |
label | string | required | Header display text |
muted | boolean | false | Muted state |
solo | boolean | false | Solo state |
locked | boolean | false | Locked (no editing) |
collapsed | boolean | false | Height collapsed |
id | string | — | Unique identifier |
| Event | Payload |
|---|---|
update:muted | boolean |
update:solo | boolean |
update:locked | boolean |
update:collapsed | boolean |
| Slot | Description |
|---|---|
header | Custom track header content |
| default | Lane content (blocks, markers, waveforms, grid) |
<VibeTimelineBlock>
Positioned region with duration. Supports drag, resize, and selection.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | TimelineBlockVariant | "generic" | Colour variant |
emphasis | TimelineBlockEmphasis | "primary" | Visual weight |
start | number | required | Start time (units) |
duration | number | required | Duration (units) |
label | string | — | Display text |
selected | boolean | false | Selection state |
resizable | boolean | true | Show resize handles |
draggable | boolean | true | Allow dragging |
id | string | — | Unique identifier |
| Event | Payload | Description |
|---|---|---|
update:selected | boolean | Selection changed |
select | PointerEvent | Block clicked/tapped |
dragstart | PointerEvent, DragMode | Drag initiated |
click | PointerEvent | Click |
dblclick | MouseEvent | Double-click (open editor) |
| Slot | Description |
|---|---|
| default | Custom block content (replaces label) |
<VibeTimelineMarker>
Point-in-time event with no duration.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | TimelineMarkerVariant | "cue" | Marker type/colour |
time | number | required | Position (units) |
label | string | — | Tooltip text |
selected | boolean | false | Selection state |
id | string | — | Unique identifier |
| Event | Payload |
|---|---|
update:selected | boolean |
select | PointerEvent |
click | PointerEvent |
dblclick | MouseEvent |
<VibeTimelinePlayhead>
Vertical position indicator. Reads from timeline context by default.
| Prop | Type | Default | Description |
|---|---|---|---|
time | number | context | Override position |
playing | boolean | context | Override playing state |
<VibeTimelineGrid>
Background snap grid rendered via CSS gradients.
| Prop | Type | Default | Description |
|---|---|---|---|
majorInterval | number | 1 | Major line spacing |
minorSubdivisions | number | 4 | Minor lines per major |
<VibeTimelineWaveform>
Canvas-based audio waveform visualisation.
| Prop | Type | Default | Description |
|---|---|---|---|
waveformStyle | TimelineWaveformStyle | "bars" | Render mode |
peaks | number[] | required | Normalised peak data (0–1) |
start | number | 0 | Position (units) |
duration | number | — | Duration (units) |
color | string | — | Custom colour override |
<VibeTimelineSelection>
Range highlight region.
| Prop | Type | Default | Description |
|---|---|---|---|
start | number | — | Range start (units) |
end | number | — | Range end (units) |
active | boolean | false | Whether visible |
Composables
useTimeline(options)
Core composable. Creates and provides the TimelineContext to all children. Called internally by <VibeTimeline> — you only need this directly if building a headless timeline.
Returns: TimelineContext + basePixelsPerUnit (writable ref).
useTimelinePlayback(options)
RAF-driven playback loop. Advances currentTime at real-time speed, handles looping and speed multiplier.
Returns: play(), pause(), togglePlayback(), seekTo(time), seekRelative(delta), rewind().
useTimelineDrag(options)
Block drag/move and edge resize with snap support. Uses pointer capture for reliable tracking.
Returns: state (reactive DragState), beginDrag(event, blockId, mode, start, duration).
useTimelineSelection(options)
Manages selected block/marker IDs and range selection.
Returns: selectedIds, select(id, additive?), clearSelection(), isSelected(id), beginRange(time), updateRange(time), endRange(), clearRange(), clearAll().
useTimelineKeyboard(options)
Keyboard shortcut handler.
| Shortcut | Action |
|---|---|
| Space | Play/pause |
| Arrow keys | Seek |
| Home | Rewind |
| Delete | Delete selected |
| Ctrl+A | Select all |
| Escape | Deselect |
| Ctrl+± / Wheel | Zoom |
| S | Toggle snap |
| L | Toggle loop |
| Ctrl+Z / Shift+Z | Undo/redo |
useTimelineZoom(options)
Zoom management with Ctrl+wheel support.
Returns: zoomIn(), zoomOut(), setZoom(ppu), zoomToFit(start, end, width), stepDensity(direction).
Provide/Inject
The root <VibeTimeline> provides a TimelineContext via the TIMELINE_INJECTION_KEY symbol. All child components inject this to read coordinate conversion, snap settings, and playback state:
ts
import { inject } from "vue";
import { TIMELINE_INJECTION_KEY } from "@vibe-labs/design-vue-timeline";
const ctx = inject(TIMELINE_INJECTION_KEY)!;
const px = ctx.timeToPixels(2.5); // time → pixel offsetDependencies
| Package | Relationship |
|---|---|
@vibe-labs/design-components-timeline | CSS + types |
@vibe-labs/core | Shared utilities |
vue | Framework (peer) |
Build
bash
npm run buildUsage Guide
Setup
ts
import {
VibeTimeline,
VibeTimelineRuler,
VibeTimelineTrack,
VibeTimelineBlock,
VibeTimelineMarker,
VibeTimelinePlayhead,
VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";
import "@vibe-labs/design-components-timeline";VibeTimeline — Practical Examples
Basic Playback Timeline
vue
<script setup lang="ts">
import { ref } from "vue";
import {
VibeTimeline,
VibeTimelineRuler,
VibeTimelineTrack,
VibeTimelineBlock,
VibeTimelinePlayhead,
VibeTimelineGrid,
useTimelinePlayback,
} from "@vibe-labs/design-vue-timeline";
const currentTime = ref(0);
const playing = ref(false);
const duration = 60;
// Clip data
const clips = ref([
{ id: "c1", start: 0, duration: 8, label: "Intro" },
{ id: "c2", start: 10, duration: 15, label: "Main segment" },
{ id: "c3", start: 30, duration: 20, label: "Outro" },
]);
</script>
<template>
<div>
<button @click="playing = !playing">{{ playing ? "Pause" : "Play" }}</button>
<button @click="currentTime = 0">Rewind</button>
<span>{{ currentTime.toFixed(2) }}s</span>
<VibeTimeline
:duration="duration"
v-model:current-time="currentTime"
v-model:playing="playing"
density="normal"
snap="beat"
snapping
>
<VibeTimelineRuler :major-interval="5" :minor-subdivisions="5" />
<VibeTimelineTrack kind="video" label="Video">
<VibeTimelineGrid :major-interval="5" />
<VibeTimelineBlock
v-for="clip in clips"
:key="clip.id"
:start="clip.start"
:duration="clip.duration"
:label="clip.label"
variant="generic"
/>
</VibeTimelineTrack>
<VibeTimelinePlayhead />
</VibeTimeline>
</div>
</template>Multi-Track Editor with Selection
vue
<script setup lang="ts">
import { ref } from "vue";
import {
VibeTimeline,
VibeTimelineRuler,
VibeTimelineTrackGroup,
VibeTimelineTrack,
VibeTimelineBlock,
VibeTimelineMarker,
VibeTimelinePlayhead,
VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";
const currentTime = ref(0);
const playing = ref(false);
const selectedBlockId = ref<string | null>(null);
</script>
<template>
<VibeTimeline
:duration="120"
v-model:current-time="currentTime"
v-model:playing="playing"
density="normal"
snapping
>
<VibeTimelineRuler :major-interval="10" :minor-subdivisions="4" />
<VibeTimelineTrackGroup label="Dialogue">
<VibeTimelineTrack kind="dialogue" label="Actor A">
<VibeTimelineGrid :major-interval="10" />
<VibeTimelineBlock
id="b1"
variant="speech"
:start="5"
:duration="8"
label="Line 1"
:selected="selectedBlockId === 'b1'"
@select="selectedBlockId = 'b1'"
/>
</VibeTimelineTrack>
<VibeTimelineTrack kind="dialogue" label="Actor B">
<VibeTimelineGrid :major-interval="10" />
<VibeTimelineBlock
id="b2"
variant="speech"
:start="15"
:duration="6"
label="Response"
:selected="selectedBlockId === 'b2'"
@select="selectedBlockId = 'b2'"
/>
</VibeTimelineTrack>
</VibeTimelineTrackGroup>
<VibeTimelineTrack kind="event" label="Cues">
<VibeTimelineMarker variant="cue" :time="10" label="Scene change" />
<VibeTimelineMarker variant="cue" :time="25" label="Music in" />
</VibeTimelineTrack>
<VibeTimelinePlayhead />
</VibeTimeline>
</template>Waveform Track
vue
<script setup lang="ts">
import {
VibeTimeline,
VibeTimelineTrack,
VibeTimelineWaveform,
VibeTimelinePlayhead,
} from "@vibe-labs/design-vue-timeline";
// Normalised peaks array 0–1
const audioPeaks = Array.from({ length: 200 }, () => Math.random() * 0.8 + 0.1);
</script>
<template>
<VibeTimeline :duration="30" :current-time="0" :playing="false">
<VibeTimelineTrack kind="audio" label="Audio">
<VibeTimelineWaveform
:peaks="audioPeaks"
:start="0"
:duration="30"
waveform-style="bars"
/>
</VibeTimelineTrack>
<VibeTimelinePlayhead />
</VibeTimeline>
</template>Composables — Usage Example
vue
<script setup lang="ts">
import { ref } from "vue";
import { useTimelinePlayback } from "@vibe-labs/design-vue-timeline";
const currentTime = ref(0);
const playing = ref(false);
const duration = 60;
const { play, pause, seekTo, rewind } = useTimelinePlayback({
currentTime,
playing,
duration: ref(duration),
looping: ref(false),
loopStart: ref(0),
loopEnd: ref(duration),
speed: ref(1),
});
</script>
<template>
<button @click="play">Play</button>
<button @click="pause">Pause</button>
<button @click="rewind">Rewind</button>
<button @click="seekTo(30)">Jump to 30s</button>
</template>Common Patterns
Inject Timeline Context in a Child Component
vue
<script setup lang="ts">
import { inject } from "vue";
import { TIMELINE_INJECTION_KEY } from "@vibe-labs/design-vue-timeline";
const ctx = inject(TIMELINE_INJECTION_KEY)!;
function getPixelOffset(timeInSeconds: number) {
return ctx.timeToPixels(timeInSeconds);
}
</script>Controlled Zoom Level
vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeTimeline } from "@vibe-labs/design-vue-timeline";
const density = ref<"compact" | "normal" | "wide">("normal");
</script>
<template>
<div>
<button @click="density = 'compact'">Zoom Out</button>
<button @click="density = 'normal'">Fit</button>
<button @click="density = 'wide'">Zoom In</button>
<VibeTimeline
:duration="60"
:current-time="0"
:playing="false"
v-model:density="density"
>
<!-- tracks -->
</VibeTimeline>
</div>
</template>