@lgs1920/timeline v1.1.1

A modular and adaptable timeline

Built for Web Awesome applications, this component uses Web Awesome controls, themes, and design tokens to provide a consistent timeline surface. Explore focused examples: a Studio-style controlled timeline, external time and zoom sliders, readonly playback, range editing, slots, and event-driven updates.

Demo 01 · Studio integration

Control the timeline from the host

Ready

The timeline stays controlled by its host. The standard time, transport, loop, and zoom controls remain inside the component. The play button emits a playback intent, then the host clock advances currentTimeMillis. The host also controls playback speed at 1×, 2×, or 5× through the top menu slot. Play opens a draggable Three.js Pac-Man companion that eats the clips in chronological order. When the next clip starts on another track, Pac-Man changes lane and leaves the current clip shortened.

The timeline is controlled by the host application. The component emits playback and seek requests, while the host clock writes the resulting position back through currentTimeMillis.

  • Use the top menu to move the playhead by ten seconds.
  • Use Shuffle in the top menu to redistribute the clips and reorder the current tracks.
  • Use the loop button in the transport area to repeat the selected range.
  • Drag the generated sources to create video, audio, text, or other clips.
  • Use the standard time and zoom controls rendered by the component.
  • Play the timeline to open the draggable Pac-Man view. Fully eaten clips disappear and increase the counter; clips interrupted by a lane change stay shortened and do not count.

External clip source

Drag a clip to the timeline Four generated clips are always ready: video, audio, text, and other. Drop one into a compatible track to choose its position; a replacement of the same type appears immediately.
Shuffle tracks and clips x1 x2 x5 -10s Back 10 seconds +10s Forward 10 seconds Preparing timeline...
Last event None

What to observe

The host receives seek intent and writes event.detail.timeMillis back to the component. It also owns the playback clock: play starts it, pause stops it, each frame updates currentTimeMillis, and the selected rate changes elapsed time without moving the playhead. The standard time and zoom controls stay inside the component. The top menu slot contains shuffle, playback speed, and 10-second seek controls. Shuffle redistributes the existing clips and reorders the current tracks without creating new ones. The clip sources are outside the custom element, so they can live in a toolbar, palette, or application menu. Four generated clip sources stay available for dragging, and each accepted drop replaces its source with a new one.

<lgs1920-timeline>
    <span slot="custom-menu">
        <wa-button id="studio-shuffle" appearance="plain" variant="brand" size="s" aria-label="Shuffle tracks and clips"><wa-icon slot="start" name="shuffle" variant="solid"></wa-icon></wa-button>
        <wa-tooltip for="studio-shuffle" placement="bottom">Shuffle tracks and clips</wa-tooltip>
        <wa-button-group label="Playback speed">
            <wa-button data-playback-rate="1" appearance="filled" variant="brand" size="s" aria-pressed="true">x1</wa-button>
            <wa-button data-playback-rate="2" appearance="filled" variant="neutral" size="s" aria-pressed="false">x2</wa-button>
            <wa-button data-playback-rate="5" appearance="filled" variant="neutral" size="s" aria-pressed="false">x5</wa-button>
        </wa-button-group>
        <wa-button appearance="plain" variant="brand" size="s" aria-label="Back 10 seconds"><wa-icon slot="start" name="arrow-rotate-left" variant="solid"></wa-icon>-10s</wa-button>
        <wa-button appearance="plain" variant="brand" size="s" aria-label="Forward 10 seconds"><wa-icon slot="start" name="arrow-rotate-right" variant="solid"></wa-icon>+10s</wa-button>
    </span>
</lgs1920-timeline>

timeline.addEventListener('lgs1920-timeline-seek', event => {
    clock.seek(event.detail.timeMillis)
})

<p>Drag a clip to the timeline</p>
<div id="generated-clip-sources"></div>
timeline.addEventListener('lgs1920-timeline-play', () => {
    timeline.playing = true
    clock.start()
})

timeline.addEventListener('lgs1920-timeline-pause', () => {
    timeline.playing = false
    clock.stop()
})

timeline.addEventListener('lgs1920-timeline-seek', event => {
    timeline.currentTimeMillis = event.detail.timeMillis
    clock.seek(event.detail.timeMillis)
})
import {CLIP_OPTION_DRAG_MIME} from '@lgs1920/timeline'

const clipSources = document.querySelector('#generated-clip-sources')
const replenishClipSources = () => {
    while (clipSources.children.length < 4) {
    const option = createRandomClipOption()
    const clip = document.createElement('wa-button')
    clip.textContent = `${option.label} · ${option.duration}s`
    clip.draggable = true
    clip.addEventListener('dragstart', event => {
        event.dataTransfer.effectAllowed = 'copy'
        event.dataTransfer.setData(CLIP_OPTION_DRAG_MIME, JSON.stringify(option))
    })
    clipSources.append(clip)
    }
}

replenishClipSources()

Drag one of the four clip types onto the timeline. Each accepted drop creates a replacement of the same type.

This simplified excerpt uses the same controlled clock as the timeline. Three.js renders the miniature ruler and clips, while the current time selects the clip to visit, moves Pac-Man from its start toward its end, and reduces the remaining clip width. Web Audio generates the chomp effect and an arcade-inspired fallback loop.

<wa-popup id="pacman-popup" anchor="studio-timeline" placement="top-end">
    <canvas id="pacman-canvas" aria-label="Pac-Man timeline companion"></canvas>
</wa-popup>
import * as THREE from 'three'

const clips = timeline.tracks
    .flatMap(track => track.clips.map(clip => ({...clip, trackId: track.id})))
    .sort((left, right) => left.start - right.start)

const pacman = new THREE.Group()
const pacmanBody = new THREE.Mesh(
    new THREE.CircleGeometry(0.36, 40, 0.2, (Math.PI * 2) - 0.4),
    new THREE.MeshBasicMaterial({
        color: getComputedStyle(document.documentElement).getPropertyValue('--wa-color-brand-60').trim() || '#ffd23f',
    }),
)
pacman.add(pacmanBody)

const updatePacman = (timeMillis, playing) => {
    const activeIndex = clips.findLastIndex(clip => clip.start * 1000 <= timeMillis)
    const clip = clips[activeIndex]
    if (!clip) return

    const nextClip = clips[activeIndex + 1]
    const progress = Math.min(1, Math.max(0,
        (timeMillis - (clip.start * 1000)) / ((clip.end - clip.start) * 1000),
    ))
    const changesTrack = nextClip && nextClip.trackId !== clip.trackId
    const biteEndMillis = changesTrack
        ? Math.min(clip.end * 1000, nextClip.start * 1000)
        : clip.end * 1000
    const visibleProgress = Math.min(progress, Math.max(0,
        (biteEndMillis - (clip.start * 1000)) / ((clip.end - clip.start) * 1000),
    ))
    pacman.position.copy(positionAtClip(clip, progress))
    clipMeshes[activeIndex].scale.x = Math.max(0.04, 1 - visibleProgress)

    const eatenCount = clips.filter((candidate, index) => {
        const followingClip = clips[index + 1]
        const changesLane = followingClip && followingClip.trackId !== candidate.trackId
        const candidateBiteEnd = changesLane
            ? Math.min(candidate.end * 1000, followingClip.start * 1000)
            : candidate.end * 1000
        return candidateBiteEnd === candidate.end * 1000 && timeMillis >= candidate.end * 1000
    }).length
    const almostEatenCount = clips.filter((candidate, index) => {
        const followingClip = clips[index + 1]
        const changesLane = followingClip && followingClip.trackId !== candidate.trackId
        const candidateBiteEnd = changesLane
            ? Math.min(candidate.end * 1000, followingClip.start * 1000)
            : candidate.end * 1000
        return candidateBiteEnd > candidate.start * 1000
            && candidateBiteEnd < candidate.end * 1000
            && timeMillis >= candidateBiteEnd
    }).length
    const result = eatenCount === clips.length
        ? 'Win'
        : almostEatenCount * 2 >= clips.length
            ? 'Lose'
            : 'In progress'
    // Render eatenCount, almostEatenCount, and result in the Pac-Man popup footer.

    if (playing) {
        pacmanAudio.playChomp()
    }
}

Demo 02 · Editing and events

Move clips and inspect the contract

Ready

Drag or trim a clip, double-click a track label, or use the playback toolbar. Right-click a track to open its context menu. The host advances the clock after a play event, so this demo shows the same integration pattern as the Studio example. Every interaction emits a namespaced event; this demo writes accepted track changes back to the element.

This example focuses on editing and event handling. Move or trim a clip, use the scissors tool to preview and commit a cut, edit a track name, open the track context menu, change visibility, and inspect the serialized event detail in the fixed-height event panel.

  • Apply event.detail.tracks when a clip or track edit is accepted.
  • Use timeline.on(name, {before, on, after}) when an action needs validation or persistence.
  • The host playback clock continues to own currentTimeMillis.
  • Track actions are contextual: Edit applies to visible editable rows, while Hide/Show and Remove follow the track configuration.
  • In cut mode, hover an eligible clip to see the dashed guide and compact duration overlay, such as 1s500ms/3s [2s500ms], then click to split it; use Shift + click for consecutive cuts.
  • Use the Undo and Redo buttons in the tools bar to move through up to 200 committed edits; the host can accept each undo or redo event through its resulting tracks.
Last event
None
timeline.on('clip-change', {
    before: event => validateClipEdit(event.detail),
    on: event => timeline.tracks = event.detail.tracks,
    after: event => console.log('edit completed', event.detail),
})

timeline.addEventListener('lgs1920-timeline-seek', event => {
    timeline.currentTimeMillis = event.detail.timeMillis
})

Demo 03 · Readonly playback

Readonly Playback Projection

The timeline is a readonly playback projection: the host controls and sends the current time through an external player control. Inside the timeline, only the playhead grip can move. Use this pattern for a video player, a presentation preview, or a monitoring screen.

Readonly mode is useful when another component owns the timeline data and current position. The host keeps sending the external player position, while the timeline exposes playback controls, fixed start/end range handles, the draggable playhead grip, and standard clip icons.

  • Use the readonly attribute with playback.transport = 'hidden' and playback.time = 'hidden' when the host owns the controls.
  • Use options.mode = 'passive' for a passive projection, or options.mode = 'review' when playback and review navigation should remain available without editing.
  • Keep the external player's time slider outside the timeline.
  • Use this pattern for monitoring views, review screens, or compact sequence summaries.
Play preview

External player paused

<lgs1920-timeline id="timeline" readonly></lgs1920-timeline>
timeline.options = {
    durationMillis: 30000,
}

player.addEventListener('timeupdate', () => {
    timeline.currentTimeMillis = player.currentTime * 1000
})

Demo 04 · Video range

Limit playback to a selected range

Drag the colored start and end handles on the ruler. The playback clock stops at the selected end, which is useful when the timeline represents an in and out range. Keyboard users can focus the handles and move them with the arrow keys.

The selected range defines the portion of the timeline that can be played. The light-blue ruler area shows the portion that will actually be recorded or played back.

  • Drag the green start handle and red end handle to change the range.
  • Use the timeline transport to inspect the current position inside the selected range.
  • The host updates the playback clock when a range change is committed.

Range: 00:06 – 00:24 · Playback paused at 00:06

timeline.options = {
    range: {startMillis: 6_000, endMillis: 24_000},
}

timeline.addEventListener('lgs1920-timeline-range-change', event => {
    console.log(event.detail.startMillis, event.detail.endMillis)
})

Demo 05 · Slots and controlled state

Compose the surrounding UI

The component owns the timeline surface while the application owns its context: the temporal slider in time-slider, footer controls in timeline-controls and footer, and application actions in custom-menu. The full slot reference is available in the documentation.

Slots let the host place application controls around the timeline without coupling those controls to the component implementation.

  • time-slider replaces the temporal slider in the playback row.
  • timeline-ruler places content above the time ruler.
  • timeline-controls adds controls alongside the built-in zoom controls in the footer.
  • custom-menu adds host actions to the timeline header.
  • overlay-text replaces the initial construction message.
  • footer adds application content at the bottom of the timeline.
timeline-ruler · external scrubber time-slider · playback scrubber timeline-controls · footer controls footer · application content custom-menu · host actions overlay-text · initial loading label
<lgs1920-timeline>
    <span slot="time-slider">...time slider...</span>
    <span slot="timeline-ruler">...ruler content...</span>
    <span slot="timeline-controls">...zoom slider...</span>
    <span slot="footer">...footer content...</span>
    <wa-button slot="custom-menu">Settings</wa-button>
</lgs1920-timeline>

Demo 06 · Keyboard shortcuts

Keep editing within reach of the keyboard

Focus the timeline surface, a range handle, a clip, a clip trim handle, or the legend divider, then use the matching shortcuts below. The divider shows the brand state while it is focused or being resized. The live status line records the last key pressed inside the Studio-style timeline.

The keyboard model follows the same controlled event flow as pointer interaction. Focus determines the target, and the timeline reports the resulting action through its namespaced events.

  • Use the timeline surface for playback, range navigation, scrolling, and zoom.
  • Focus a playhead or range handle for frame-sized movement.
  • Focus a clip to move, trim, duplicate, mask, enable, or delete it.

Click the Studio timeline, then press a shortcut.

Timeline keyboard shortcuts
Focus or context Keys Action
Timeline surfaceSpaceToggle local playback.
Timeline surfaceHome · EndMove the playhead to the selected range start or end.
Timeline surfaceShift + / Move the playhead to the selected range boundary.
Timeline surfaceShift + / Scroll tracks to the top or bottom.
Timeline surface / Increase or decrease row height.
Timeline surface / Zoom the ruler horizontally when no clip is selected.
Scrollbar railPageUp · PageDown · / Scroll one viewport in the focused direction.
Playhead grip / Move by keyboardStepSeconds.
Playhead gripShift + / Move by ten keyboard steps.
Playhead gripAlt + / Jump to the range minimum or maximum.
Range handle / Move the focused boundary by one keyboard step.
Range handleShift + / Move the focused boundary by ten keyboard steps.
Clip / Move the selected clip by one rendered pixel.
ClipAlt + / Move the selected clip by ten rendered pixels.
ClipDelete · BackspaceDelete the focused editable clip.
ClipMod + CStart a copy placement ghost; click to place it.
ClipMod + DDuplicate the clip immediately after itself.
Editable timelineMod + KCut every eligible clip at the current playhead.
Editable timelineMod + ZUndo the latest committed edit.
Editable timelineMod + Shift + Z · Mod + YRedo the latest undone edit.
ClipMMask or reveal the clip.
ClipVEnable or disable the clip.
Non-movable clipEnter · SpaceSelect the clip.
Clip trim handle / Trim the focused edge by one keyboard step.
Clip trim handleShift + / Trim the focused edge by ten keyboard steps.
Any active editEscapeCancel a copy, drag, trim, cut mode, or context menu; clear clip selection.
Legend divider / Resize the track legend.
Legend dividerShift + / · Home · End · EnterChange the resize step, select the minimum or maximum, or collapse and restore the legend.
Track label editorEnter · EscapeCommit or cancel the label edit.

Mod means Ctrl on Windows and Linux, and on macOS. Wheel gestures are also available on the timeline surface: Shift or Alt + wheel changes row height, while Meta + wheel changes horizontal ruler zoom. Ctrl + wheel remains available to the browser.

timeline.options = {
    keyboardZoomActive: true,
    keyboardStepSeconds: 0.1,
}

timeline.addEventListener('lgs1920-timeline-seek', event => {
    timeline.currentTimeMillis = event.detail.timeMillis
})

// The component handles the shortcuts according to the focused part.
// The host only needs to keep controlled state synchronized.