Clip Timeline
A multi-track editing timeline: drag and trim clips with grid snapping, a draggable playhead with playback, a ruler that re-ticks as you zoom, comment pins and audio waveforms.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
The multi-track timeline from a video editor, as a component you can drop into a web page: track headers on the left (name, mute, lock) and a horizontally scrolling time area on the right. Clips can be dragged and trimmed from either edge, snapping to minor ticks and other clip edges as you go; an overlap on the same track is rejected and the clip springs back. Drag the playhead or click the ruler to seek, and press Space to play via rAF; the ruler picks a new major tick step as you zoom. Audio clips draw a waveform, and comments sit as pins below the ruler that expand on hover.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/clip-timeline.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ClipTimeline/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/clip-timeline.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/clip-timeline.
Usage
import * as React from "react";
import {
ClipTimeline,
type TimelineTrack,
} from "@/components/ui/clip-timeline";
const initialTracks: TimelineTrack[] = [
{
id: "video",
name: "Video",
clips: [
{ id: "v1", start: 0, duration: 5, label: "Opening shot", kind: "video" },
{ id: "v2", start: 5, duration: 8, label: "Interior pan", kind: "video" },
],
},
{
id: "audio",
name: "Audio",
clips: [{ id: "a1", start: 0, duration: 12, label: "Background music", kind: "audio" }],
},
{
id: "subtitle",
name: "Captions",
clips: [{ id: "t1", start: 2, duration: 4, label: "Welcome in", kind: "text" }],
},
];
export function Editor() {
const [tracks, setTracks] = React.useState(initialTracks);
const [time, setTime] = React.useState(0);
return (
<ClipTimeline
tracks={tracks}
onTracksChange={setTracks}
duration={30}
currentTime={time}
onTimeChange={setTime}
comments={[{ id: "c1", t: 6, text: "This transition is too fast" }]}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
tracks | TimelineTrack[] | — | Controlled track data; uncontrolled when omitted |
defaultTracks | TimelineTrack[] | [] | Initial track data in uncontrolled mode |
onTracksChange | (tracks: TimelineTrack[]) => void | — | Called after a move, trim, delete, mute or lock |
duration | number | 30 | Total project length (seconds) |
currentTime | number | — | Controlled playhead time (seconds) |
defaultCurrentTime | number | 0 | Initial playhead time in uncontrolled mode |
onTimeChange | (time: number) => void | — | Called when the playhead time changes (playback, dragging, clicking the ruler) |
playing | boolean | — | Controlled playing state |
defaultPlaying | boolean | false | Initial playing state in uncontrolled mode |
onPlayingChange | (playing: boolean) => void | — | Called when the playing state toggles |
zoom | number | — | Controlled zoom factor (0.25–4); width per second is 80px × zoom |
defaultZoom | number | 1 | Initial zoom factor in uncontrolled mode |
onZoomChange | (zoom: number) => void | — | Called when the zoom factor changes |
snap | boolean | true | Snap to minor ticks, other clip edges and the playhead (within 6px) while dragging |
comments | TimelineComment[] | [] | Comments pinned below the ruler |
showComments | boolean | true | Whether to show comment pins |
loop | boolean | false | Whether playback loops back to the start at the end |
height | number | — | Max height of the timeline area (px); overflow scrolls vertically with the ruler pinned at the top |
bounceStiffness | number | 500 | Stiffness of the bounce-back spring when a same-track overlap is rejected (damping is fixed at 35) |
className | string | — | Merged into the root container |
TimelineTrack
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique track id |
name | string | — | Track name; also the row's accessible name |
clips | TimelineClip[] | — | Clips on the track |
muted | boolean | false | Muted (clips are dimmed) |
locked | boolean | false | Locked (clips cannot be moved, trimmed or deleted) |
TimelineClip
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique clip id |
start | number | — | Start time (seconds) |
duration | number | — | Length (seconds), minimum 0.2 |
label | string | — | Display name |
kind | "video" | "audio" | "text" | "image" | — | Clip kind; sets the color and icon |
peaks | number[] | — | Audio waveform peaks (0–1); when omitted a deterministic fake waveform is generated from id |
TimelineComment
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique comment id |
t | number | — | Time the comment is pinned to (seconds) |
text | string | — | Comment text |
author | string | — | Comment author |
Also exports formatTimecode (mm:ss.f formatting) and the ClipKind and ClipTimelineProps types.
How it works
- Re-ticking ruler: width per second is 80px × zoom. The major tick step is the smallest of 0.1 / 0.5 / 1 / 2 / 5 / 10 / 30 / 60 seconds that spaces ticks at least 64px apart, with five minor ticks per major; below 1 second labels switch to mm:ss.f
- Snapping: while moving or trimming, a moving edge within 6px of a minor tick, another clip edge, the project start/end or the playhead aligns to it; set
snaptofalseto turn it off - Changing tracks: clips can be dragged up or down onto unlocked tracks of the same kind (or empty ones); a track's kind is its first clip's kind
- Overlap rejection: if a drop overlaps another clip on the target track, no change is emitted and the clip springs back (adjustable stiffness, damping 35); a red outline warns you while dragging
- Playback: time advances with
requestAnimationFrameandperformance.nowdeltas, so dropped frames do not skew progress; it stops at the end, or starts over withloop. The view auto-scrolls horizontally to follow the playhead - Colors: the four clip colors use
--wb-ct-video,--wb-ct-audio,--wb-ct-textand--wb-ct-image, defaulting to desaturated blue, green, purple and amber; video clips also get diagonal stripes - Waveforms: audio clips draw SVG bars from
peaks; without them a deterministic fake waveform is derived from a hash ofid, so SSR and client match
Accessibility
- The track area is
role="grid"(aria-label"Timeline"), each track is arole="row"labelled with its name, and clips arerole="gridcell"with labels like "Video Opening shot, starts at 00:00, 5 seconds long"; selection is exposed witharia-selected - Clips use a roving tabindex: Tab enters a single clip, Up/Down moves to the nearest clip on the adjacent track, Left/Right nudges selected clips by 0.1s (1s with Shift), Alt + Left/Right changes length, Delete removes, and Enter toggles selection (Enter during IME composition is ignored)
- Space plays / pauses (not intercepted while an input or button has focus), and +/- zooms
- The playhead handle is a
role="slider"with the timecode asaria-valuetext, operable with the arrow keys (Shift for bigger steps), Home and End - Comment pins are buttons that expand a bubble (
role="tooltip") on hover or focus and seek to their time when clicked; move and delete results are announced through anaria-liveregion - When the user's system has reduce motion enabled, rejected clips snap straight back without the spring, comment bubbles fade without scaling, and auto-scroll following the playhead jumps instantly
Gradient Builder
Edit gradients by dragging color stops, with an angle dial, one-click CSS copying, and a preset library.
Cart Arc
An arc into the bag — clicking add-to-cart sends the product thumbnail flying along a bézier arc toward the cart, where the icon squashes like jelly and the badge number pops as it increments; rapid clicks send several in flight at once.