A controlled timeline Web Component for Web Awesome applications.
Build editing interfaces with tracks, clips, playback, range selection, zoom, and external clip sources.
Live demo · API documentation · npm · Repository
The application owns the playback clock, persistence, and business rules. The timeline renders the state provided by its host and emits namespaced events for user intent and editing results.
The component is designed for Web Awesome applications. It uses Web Awesome components, themes, and design tokens for its controls and visual integration.
The current release is 1.2.0.
| Timeline editing | Host integration |
|---|---|
| Multiple tracks with video, audio, marker, or custom clip kinds | Controlled timeline, tracks, playhead, and playback state |
| Move, trim, snap, reorder, extend, mask, and duplicate clips | Namespaced events with optional cancelable before and after hooks |
| Range handles, frame stepping, keyboard shortcuts, and zoom | Web Component slots, external controls, and a React adapter |
| Read-only projections for compact sequence summaries | Collision policies and application-defined clip actions |
The package supports Bun 1.4 or newer and Node.js 20 or newer.
bun add @lgs1920/timelineImport the Web Awesome stylesheet in the host application, then register the timeline element:
import '@awesome.me/webawesome/dist/styles/webawesome.css'
import '@lgs1920/timeline'The stylesheet remains in the host application so the host controls when the
Web Awesome styles are loaded. The package registers the Web Awesome components
required by the timeline and registers <lgs1920-timeline>.
<lgs1920-timeline id="timeline" aria-label="Video timeline"></lgs1920-timeline>const timeline = document.querySelector('#timeline')
timeline.options = {
mode: 'edit',
durationMillis: 60_000,
view: {visible: true},
}
timeline.tracks = [
{
id: 'camera',
label: 'Camera',
clips: [
{id: 'intro', label: 'Introduction', kind: 'video', start: 0, end: 8},
],
},
]
timeline.currentTimeMillis = 0
timeline.playing = false
timeline.looping = false
timeline.addEventListener('lgs1920-timeline-seek', event => {
timeline.currentTimeMillis = event.detail.timeMillis
})
timeline.on('clip-change', {
before: event => validateClipEdit(event.detail),
on: event => timeline.tracks = event.detail.tracks,
after: event => console.log('clip edit completed', event.detail),
})Timeline duration and range values use milliseconds. Clip start and end
values use seconds. The component emits user intent and editing results; the
host decides whether to persist the resulting state or connect it to a media
player.
Global markers render in an optional rail directly below the time ruler. The rail is empty and collapsed by default:
timeline.markers = [
{id: 'intro', timeMillis: 4_000, label: 'Intro', icon: 'diamond'},
{id: 'chapter', timeMillis: 15_000, label: 'Chapter', icon: 'bookmark'},
]Use <div slot="marker-rail">...</div> for application-owned rail content.
The default icon is diamond; the icon property accepts an icon name, an
image descriptor, or a DOM/SVG node. A marker-specific DOM/SVG icon can also be
provided with marker-icon-{id}. The marker rail grows to its rendered content
and keeps the track titles aligned with it. Right-click the marker rail for
Add, or a marker for Edit, Move, and Remove according to its
permissions. These actions emit lgs1920-timeline-add-marker,
lgs1920-timeline-remove-marker, lgs1920-timeline-marker-display-change,
lgs1920-timeline-edit-marker, and lgs1920-timeline-move-marker; the host
accepts them by assigning the resulting event.detail.markers back to
timeline.markers.
Edit opens an aligned popup for the marker label and color and emits an
edit-marker event when the changes are saved.
Markers are controlled by the application:
timeline.markers = [...timeline.markers, {
id: 'live-marker',
timeMillis: 18_500,
label: 'Live marker',
icon: 'diamond',
editable: true,
movable: true,
}]
const acceptMarkerChange = event => {
timeline.markers = event.detail.markers
}
for (const name of ['add-marker', 'remove-marker', 'marker-display-change', 'edit-marker', 'move-marker']) {
timeline.addEventListener(`lgs1920-timeline-${name}`, acceptMarkerChange)
}The component does not advance the application clock. It emits playback
requests, while the host starts or stops its own clock and writes the current
position back through currentTimeMillis.
An accepted play request always starts at the active range start. The host clock must stop at the active range end, or return to the range start when looping is enabled.
The loop button emits a controlled loop-change request. Apply its
detail.looping value to the host playback clock and to timeline.looping.
Set timeline.noLoopMode = true to hide that button.
timeline.addEventListener('lgs1920-timeline-play', () => {
timeline.playing = true
media.play()
})
timeline.addEventListener('lgs1920-timeline-pause', () => {
timeline.playing = false
media.pause()
})
timeline.addEventListener('lgs1920-timeline-seek', event => {
const timeMillis = event.detail.timeMillis
timeline.currentTimeMillis = timeMillis
media.currentTime = timeMillis / 1000
})
media.addEventListener('timeupdate', () => {
timeline.currentTimeMillis = media.currentTime * 1000
})The same loop applies to stop, restart, and frame navigation. For direct
host-controlled movement without emitting a seek event, use setTime(),
advance(durationMillis), or rewind(durationMillis).
Use grouped options for configuration:
timeline.options = {
mode: 'edit',
durationMillis: 60_000,
playback: {loop: 'toggle', transport: 'visible', time: 'visible', timeSlider: 'visible'},
view: {visible: true, zoomSlider: true},
range: {startMillis: 0, endMillis: 60_000},
layout: {legend: {width: 150}},
editing: {clipMenu: true, collisionPolicy: 'prevent'},
}tracks, currentTimeMillis, playing, and looping remain separate
controlled properties. Subscribe to one canonical event with on() when the
action needs lifecycle hooks:
timeline.on('seek', event => console.log(event.detail), {
before: event => event.detail.timeMillis < 0 && event.preventDefault(),
after: event => console.log('seek completed', event.detail),
})The DOM event lgs1920-timeline-seek is dispatched for the main action.
before-* and after-* DOM events and the old React callback props are no
longer part of the API. The React adapter uses events={{seek: {before, on, after}}} and callbacks receive (detail, event).
Tracks are serializable objects identified by a stable id. Each track can
define its label, icon, color classes, visibility, accepted clip kinds, and
editing policies. Clips use stable identifiers and second-based start and
end positions.
timeline.tracks = [
{
id: 'camera#main',
label: 'Main camera',
icon: 'video',
clips: [
{id: 'intro#001', label: 'Intro', kind: 'video', start: 0, end: 8},
{id: 'scene#002', label: 'Scene', kind: 'video', start: 12, end: 36},
],
},
{
id: 'music',
label: 'Music',
icon: 'music',
clips: [
{id: 'music#001', label: 'Opening theme', kind: 'audio', start: 0, end: 42},
],
},
]The component exposes four interaction contracts:
interactive: false: passive projection with no transport, scrubbing, selection, menus, editing, or drag targets.interactive: truewitheditable: false: playback, scrubbing, selection, and keyboard navigation remain available while editing and context menus are disabled.interactive: truewitheditable: true: playback and the configured track and clip editing operations are available. Right-click a track to open itsEdit,Hide/Show, orRemoveactions when the data allows them.readonlyHTML attribute: playback controls and the playhead grip remain available; range handles are fixed and editing, scrubbing, selection, menus, and drag targets are disabled. Visible and enabled track names keep their normal text color while remaining non-editable.
readonly is also available as a boolean element property. It is separate from
the timeline configuration object.
<lgs1920-timeline readonly></lgs1920-timeline>Clip sources can live in a toolbar, palette, or application menu. They do not need to be placed in a timeline slot. Serialize a clip option with the exported MIME constant and drop it on an editable track:
<wa-button id="clip-source" draggable="true">
Add a clip by dragging it
</wa-button>
<lgs1920-timeline id="timeline"></lgs1920-timeline>import {CLIP_OPTION_DRAG_MIME} from '@lgs1920/timeline'
document.querySelector('#clip-source').addEventListener('dragstart', event => {
const option = {
key: 'random-clip',
label: 'Random clip',
kind: 'video',
duration: 7.4,
clip: {
icon: 'camera',
colorClasses: ['wa-neutral', 'wa-neutral-purple'],
},
}
event.dataTransfer.effectAllowed = 'copy'
event.dataTransfer.setData(CLIP_OPTION_DRAG_MIME, JSON.stringify(option))
})The pointer represents the clip center during the drag. Outside a drop track,
the host can show a floating drag representation; over a compatible track, the
timeline shows the clip preview. The timeline applies the configured snap and
collision rules and previews rejected placements. If
the resulting add-clip event has detail.clip === null, the host can ask the
user to choose another track or drop position.
Set range.startMillis and range.endMillis to limit the active playback range.
The built-in time slider is displayed by default in the left playback area.
Set noTimeSlider: true to hide it. Set showZoomSlider to display the component's built-in
horizontal zoom control in the footer. The footer also contains the built-in
horizontal-fit and vertical-zoom buttons unless noZoomControls is enabled.
Set playback.transport to 'hidden' to remove the built-in transport buttons
and loop toggle. Set playback.time to 'hidden' to remove the current and
total time labels. This is useful when a host provides an external player and
time slider.
Editable timelines place a scissors editing tool immediately to the left of
the time slider. Hovering a clip in cut mode shows a dashed guide and clicking
splits it at that position. Ctrl/Cmd+K cuts eligible clips at the playhead;
normal clicks exit cut mode, while Shift-click keeps it active for consecutive
cuts. Escape or clicking the scissors button exits cut mode. Set
view.tools: 'hidden' or the tools-hidden attribute/property to hide the
editing tool. The cut overlay uses the compact format
1s500ms/3s [2s500ms]: elapsed time within the clip, clip duration, and
timeline position. Units are concatenated without spaces, and zero-valued
units are omitted.
The same tools bar provides per-timeline Undo and Redo controls for up to 200
accepted editing operations. Preview and canceled interactions do not enter
history, and a new edit after Undo clears Redo.
External controls can use the timeline-ruler, timeline-controls, and
footer slots when the host needs a different layout.
timeline.options = {
durationMillis: 60_000,
range: {startMillis: 6_000, endMillis: 24_000},
playback: {transport: 'hidden', time: 'hidden', timeSlider: 'visible'},
view: {zoomSlider: true, zoomControls: 'visible'},
}The start and end handles remain visible as the viewport moves. During playback, the component follows the playhead when the active range extends beyond the visible surface.
While playback is active, track and clip editing is locked: context menus, title editing, drag operations, range changes, and insertion are disabled. Playback controls and the built-in time and zoom sliders remain available.
React is an optional peer dependency:
import {LGS1920TimelineReact} from '@lgs1920/timeline/react'
export const VideoTimeline = props => <LGS1920TimelineReact {...props} />The adapter accepts the same timeline, tracks, currentTimeMillis, and
playing model as the Web Component. Event callbacks receive (detail, event).
The complete callback mapping is in the API documentation.
- Complete component reference: properties, data models, slots, events, editing behavior, CSS parts, methods, and accessibility.
- Functional, technical, and software specifications
- Live demo: controlled playback, clip editing, readonly playback, range playback, slots, and keyboard interaction.
- NPM package
- Source repository
bun install
bun run verify
bun run pack:checkThe verification command runs the tests, linting, package build, consumer export check, and static demo build. Build and serve the demo independently:
bun run demo:build
bun run demo:serveThe local demo is served at http://localhost:4174. The generated Pages
directory is demo/dist/; the package bundle is generated in dist/. These
directories are build outputs and must not be edited manually.
After cloning, install the repository hooks once if you need the project header and staged-file checks:
bun run git:hooks:installPreview release metadata before changing the working tree:
bun run publish --preview
bun run publish --minor --previewAfter reviewing the preview on a clean, validated working tree, the release script can update the version, create the annotated tag, and publish the package:
bun run publish
bun run publish --minor
bun run publish --majorMIT. See LICENSE.md.