App Shells & Panels

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.

How to install Pro components →See the plans →

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>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

1
500
Active edited usageDefault props
<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

PropTypeDefaultDescription
tracksTimelineTrack[]—Controlled track data; uncontrolled when omitted
defaultTracksTimelineTrack[][]Initial track data in uncontrolled mode
onTracksChange(tracks: TimelineTrack[]) => void—Called after a move, trim, delete, mute or lock
durationnumber30Total project length (seconds)
currentTimenumber—Controlled playhead time (seconds)
defaultCurrentTimenumber0Initial playhead time in uncontrolled mode
onTimeChange(time: number) => void—Called when the playhead time changes (playback, dragging, clicking the ruler)
playingboolean—Controlled playing state
defaultPlayingbooleanfalseInitial playing state in uncontrolled mode
onPlayingChange(playing: boolean) => void—Called when the playing state toggles
zoomnumber—Controlled zoom factor (0.25–4); width per second is 80px × zoom
defaultZoomnumber1Initial zoom factor in uncontrolled mode
onZoomChange(zoom: number) => void—Called when the zoom factor changes
snapbooleantrueSnap to minor ticks, other clip edges and the playhead (within 6px) while dragging
commentsTimelineComment[][]Comments pinned below the ruler
showCommentsbooleantrueWhether to show comment pins
loopbooleanfalseWhether playback loops back to the start at the end
heightnumber—Max height of the timeline area (px); overflow scrolls vertically with the ruler pinned at the top
bounceStiffnessnumber500Stiffness of the bounce-back spring when a same-track overlap is rejected (damping is fixed at 35)
classNamestring—Merged into the root container

TimelineTrack

FieldTypeDefaultDescription
idstring—Unique track id
namestring—Track name; also the row's accessible name
clipsTimelineClip[]—Clips on the track
mutedbooleanfalseMuted (clips are dimmed)
lockedbooleanfalseLocked (clips cannot be moved, trimmed or deleted)

TimelineClip

FieldTypeDefaultDescription
idstring—Unique clip id
startnumber—Start time (seconds)
durationnumber—Length (seconds), minimum 0.2
labelstring—Display name
kind"video" | "audio" | "text" | "image"—Clip kind; sets the color and icon
peaksnumber[]—Audio waveform peaks (0–1); when omitted a deterministic fake waveform is generated from id

TimelineComment

FieldTypeDefaultDescription
idstring—Unique comment id
tnumber—Time the comment is pinned to (seconds)
textstring—Comment text
authorstring—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 snap to false to 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 requestAnimationFrame and performance.now deltas, so dropped frames do not skew progress; it stops at the end, or starts over with loop. The view auto-scrolls horizontally to follow the playhead
  • Colors: the four clip colors use --wb-ct-video, --wb-ct-audio, --wb-ct-text and --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 of id, so SSR and client match

Accessibility

  • The track area is role="grid" (aria-label "Timeline"), each track is a role="row" labelled with its name, and clips are role="gridcell" with labels like "Video Opening shot, starts at 00:00, 5 seconds long"; selection is exposed with aria-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 as aria-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 an aria-live region
  • 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