Skip to content

Repository files navigation

@lgs1920/timeline

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.

Overview

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

Installation

The package supports Bun 1.4 or newer and Node.js 20 or newer.

bun add @lgs1920/timeline

Import 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>.

Quick start

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

API markers

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)
}

Controlled playback

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).

Public configuration and events

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 and clips

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: true with editable: false: playback, scrubbing, selection, and keyboard navigation remain available while editing and context menus are disabled.
  • interactive: true with editable: true: playback and the configured track and clip editing operations are available. Right-click a track to open its Edit, Hide/Show, or Remove actions when the data allows them.
  • readonly HTML 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>

External clip sources

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.

Range playback and built-in controls

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 adapter

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.

Documentation

Development

bun install
bun run verify
bun run pack:check

The 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:serve

The 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:install

Release

Preview release metadata before changing the working tree:

bun run publish --preview
bun run publish --minor --preview

After 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 --major

License

MIT. See LICENSE.md.

About

A modular and adaptable timeline built for Web Awesome applications

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages