📚 Full documentation: github.com/pip-install-python/dash-nle-timeline — complete API reference and examples in the repository. This page is the quick-start overview.
Installation
pip install dash-nle-timeline
Introduction
A calendar or scheduler widget thinks in dates. A video editor needs an integer frame grid, a real draggable playhead, snapping to clip edges / markers / grid, audio waveforms, and edits expressed as data. dash-nle-timeline is that — a purpose-built non-linear-editor (NLE) timeline for Dash. Only small JSON travels over callbacks (the control plane); your media stays on the media plane (a <video>, an audio engine, a server render) driven off the timeline's outputs.
The package ships two components:
DashNleTimeline— frame ruler, stacked track lanes (video/audio/camera), clip rectangles, a draggable playhead, snapping, trim/resize/split/delete, waveforms from precomputedpeaks,ctrl/⌘ + wheelzoom, keyboard shortcuts, and an imperativecommandprop.DashNleScene— a Hybrid DOM + Canvas preview compositor driven byplayhead, with overlaylayers, atilesetbackground, and achildrenslot that pairs withdash-leaflet2for map-focused editing (fly a map from a camera keyframe track while compositing video on top).
Quick Start
Frames — not seconds — are the canonical unit (seconds = frame / fps). Give the timeline an explicit height via style, define tracks, then place clips on them by trackId. Audio lanes paint waveforms from precomputed peaks — the component paints bars only, it never decodes audio.
# File: docs/dash_nle_timeline/quick_start.py
import math
import dash_nle_timeline as nle
import dash_mantine_components as dmc
# Precomputed audio peaks (magnitude format: one positive value per bucket, 0..1).
# In a real app you would compute these server-side from the audio file.
PEAKS = [abs(math.sin(i / 7)) * 0.8 + 0.15 for i in range(220)]
component = dmc.Paper(
withBorder=True,
p="md",
children=dmc.Stack(
[
dmc.Text("A static three-track timeline — scrub the playhead, "
"drag clips, trim edges, or press S to split.", size="sm", c="dimmed"),
nle.DashNleTimeline(
id="nle-quick-start-tl",
fps=30,
duration=300,
pixelsPerSecond=90,
tracks=[
{"id": "v1", "kind": "video", "label": "V1"},
{"id": "v2", "kind": "video", "label": "V2"},
{"id": "a1", "kind": "audio", "label": "A1"},
],
clips=[
{"id": "c1", "trackId": "v1", "start": 0, "duration": 90,
"label": "intro.mp4", "color": "indigo"},
{"id": "c2", "trackId": "v1", "start": 100, "duration": 120,
"label": "main.mp4", "color": "teal"},
{"id": "c3", "trackId": "v2", "start": 60, "duration": 80,
"label": "overlay.png", "color": "orange"},
{"id": "c4", "trackId": "a1", "start": 0, "duration": 220,
"label": "music.wav",
"peaks": PEAKS, "peaksFormat": "magnitude", "peaksPerFrame": 1},
],
markers=[
{"id": "m1", "frame": 100, "label": "cut", "color": "gold"},
],
style={"width": "100%", "height": "260px"},
),
],
gap="sm",
),
)
Python Owns Clips — Apply lastEdit in a Callback
This is the core pattern of the component. Every committed edit (move, resize, trim, split, delete, select, seek) is reported as a single lastEdit dict. It carries a timestamp, so a Dash Input on it always fires — even for repeated identical edits.
The component applies edits to its own optimistic copy of clips (so they persist visually without a round-trip), but Python is the source of truth: a callback reads lastEdit, applies it to the clip list, and writes clips back. Writing back is how you validate, override, or persist edits — reject an edit by returning the unmodified list.
# File: docs/dash_nle_timeline/edits_callback.py
import json
import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State, no_update
component = dmc.Paper(
withBorder=True,
p="md",
children=dmc.Stack(
[
dmc.Text("Move, resize, trim, or split a clip — every committed edit "
"arrives in Python as one `lastEdit` dict.", size="sm", c="dimmed"),
nle.DashNleTimeline(
id="nle-edits-tl",
fps=30,
duration=240,
tracks=[
{"id": "v1", "kind": "video", "label": "V1"},
{"id": "v2", "kind": "video", "label": "V2"},
],
clips=[
{"id": "c1", "trackId": "v1", "start": 0, "duration": 70,
"label": "shot A", "color": "indigo"},
{"id": "c2", "trackId": "v1", "start": 90, "duration": 80,
"label": "shot B", "color": "teal"},
{"id": "c3", "trackId": "v2", "start": 40, "duration": 60,
"label": "title", "color": "orange"},
],
style={"width": "100%", "height": "200px"},
),
dmc.Text("Last edit event:", size="sm", fw=500),
dmc.Code(id="nle-edits-out", block=True,
children="(interact with the timeline)"),
],
gap="sm",
),
)
@callback(
Output("nle-edits-out", "children"),
Input("nle-edits-tl", "lastEdit"),
prevent_initial_call=True,
)
def show_edit(edit):
return json.dumps(edit, indent=2)
# Python owns `clips`: apply each reported edit to the list and feed it back.
# The component edits its own copy optimistically, so writing clips back is how
# you validate, override, or persist the result.
@callback(
Output("nle-edits-tl", "clips"),
Input("nle-edits-tl", "lastEdit"),
State("nle-edits-tl", "clips"),
prevent_initial_call=True,
)
def apply_edit(edit, clips):
if not edit:
return no_update
by_id = {c["id"]: dict(c) for c in clips}
edit_type = edit.get("type")
clip = by_id.get(edit.get("clipId"))
if edit_type == "move" and clip:
clip["start"] = edit["start"]
clip["trackId"] = edit.get("trackId", clip["trackId"])
elif edit_type in ("resize", "trim") and clip:
for key in ("start", "duration", "inPoint"):
if key in edit:
clip[key] = edit[key]
elif edit_type == "delete" and clip:
del by_id[clip["id"]]
else:
# select / seek / split / paste etc. — nothing to write back here
return no_update
return list(by_id.values())
Playback and the Playhead
Setting playing=True starts a requestAnimationFrame loop that advances playhead at fps (writes throttled by playheadThrottleMs). The component plays no media itself — drive a real <video>, an audio engine, or a DashNleScene off the playhead output. The playhead prop round-trips: the component writes it on scrub/seek/playback, and a callback may write it to move the playhead programmatically. loop=True wraps back to frame 0 at duration.
# File: docs/dash_nle_timeline/playback.py
import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State
FPS = 30
DURATION = 240
component = dmc.Paper(
withBorder=True,
p="md",
children=dmc.Stack(
[
dmc.Text("Playback is a requestAnimationFrame loop driven by the "
"`playing` prop — the component plays no media itself, it just "
"advances `playhead` at the configured fps.", size="sm", c="dimmed"),
nle.DashNleTimeline(
id="nle-playback-tl",
fps=FPS,
duration=DURATION,
loop=True,
tracks=[
{"id": "v1", "kind": "video", "label": "V1"},
],
clips=[
{"id": "c1", "trackId": "v1", "start": 0, "duration": 100,
"label": "scene 1", "color": "indigo"},
{"id": "c2", "trackId": "v1", "start": 110, "duration": 110,
"label": "scene 2", "color": "teal"},
],
style={"width": "100%", "height": "160px"},
),
dmc.Group(
[
dmc.Button("Play", id="nle-playback-btn", color="teal", size="sm"),
dmc.Text("frame 0 | 0.000s", id="nle-playback-out",
size="sm", ff="monospace", c="dimmed"),
],
gap="md",
),
],
gap="sm",
),
)
@callback(
Output("nle-playback-tl", "playing"),
Output("nle-playback-btn", "children"),
Input("nle-playback-btn", "n_clicks"),
State("nle-playback-tl", "playing"),
prevent_initial_call=True,
)
def toggle_play(_n, playing):
now_playing = not bool(playing)
return now_playing, ("Pause" if now_playing else "Play")
@callback(
Output("nle-playback-out", "children"),
Input("nle-playback-tl", "playhead"),
)
def show_playhead(frame):
frame = frame or 0
return f"frame {frame} | {frame / FPS:.3f}s"
Keyboard shortcuts (enabled by default via keyboard): Space play/pause, arrows step (Shift = 1s), S split at playhead, Delete, ⌘/Ctrl+C/V/D copy/paste/duplicate, ⌘/Ctrl+A select all, Home/End, +/- zoom. The imperative command prop dispatches the same actions from Python (play, pause, seek, splitAtPlayhead, zoomToFit, …), de-duped by a unique id.
DashNleScene
DashNleScene is the preview side: a compositor stage that renders, at the current playhead, a background — a dash-leaflet2 Map in children and/or a native XYZ tileset — then overlay layers (image / video / text) with per-layer [start, end) visibility windows and a screen-space camera. Sync playhead and playing from a DashNleTimeline, and drive the tileset.viewport from a camera-kind track's cameraKeyframes to fly the map while compositing video on top.
import dash_nle_timeline as nle
scene = nle.DashNleScene(
id="nle-scene",
fps=30,
playhead=0, # sync from DashNleTimeline
resolution=[1280, 720],
tileset={
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"viewport": {"center": [48.8566, 2.3522], "zoom": 12},
},
layers=[
{"id": "title", "kind": "text", "text": "Paris flyover",
"rect": [0.05, 0.05, 0.9, 0.2], "start": 0, "end": 90},
],
style={"width": "100%"},
)
For full map interactivity (pan/zoom/controls/flyTo) put a dash-leaflet2 Map in children instead of using tileset.
DashNleTimeline Props
| Property | Type | Default | Description |
|---|---|---|---|
id | string | - | Component ID for Dash callbacks. |
fps | number | 30 | Frames per second — defines the integer-frame grid and frame↔second conversions. |
duration | number | 300 | Total timeline length in integer frames. |
playhead | number | - | Current playhead position in integer frames. Written on scrub/seek/playback; writable from callbacks. |
playing | boolean | - | When True, a rAF loop advances playhead at fps. The component plays no media itself. |
loop | boolean | False | Loop back to 0 when the playhead reaches duration. |
playheadThrottleMs | number | 60 | Min ms between playhead writes during playback. |
tracks | list of dicts | - | Track lanes, top-to-bottom. Keys: id*, kind ('video'/'audio'/'camera'), label, height, locked, muted, color. |
clips | list of dicts | - | Clips on tracks. Keys: id, trackId, start, duration (frames), inPoint, label, color, src, peaks, peaksFormat ('minmax'/'magnitude'), peaksPerFrame, thumbnails, locked. Python is the source of truth. |
lastEdit | dict | - | [Readonly] Most recent committed edit. Keys: type (move/resize/trim/split/delete/select/seek/copy/paste), timestamp, clipId, clipIds, trackId, start, duration, inPoint, frame, resultIds, snapped. |
markers | list of dicts | - | Labeled time points: visual guides + snap targets. Keys: id, frame, label, color. |
cameraKeyframes | list of dicts | - | Keyframes for 'camera' tracks. Keys: id, trackId, time, center [lat, lng], zoom*, bearing, easing, label. |
selectedClipIds | list of strings | - | Currently selected clip ids. Round-trips; writable from Python. |
selectedClipId | string | - | [Readonly] The single selected clip id, or None when zero/many selected. |
command | dict | - | Imperative command, de-duped by id. Keys: id, type (play/pause/togglePlay/seek/stepFrame/splitAtPlayhead/deleteSelected/copy/paste/duplicate/selectAll/zoomToFit/zoomTo/scrollToFrame), payload. |
pixelsPerSecond | number | 120 | Horizontal zoom as pixels per second. |
minPixelsPerSecond | number | 8 | Lower clamp for wheel-zoom. |
maxPixelsPerSecond | number | 4000 | Upper clamp for wheel-zoom. |
scrollX | number | - | Horizontal scroll offset of the lane viewport, in CSS px from frame 0. |
snapping | boolean | True | Master enable for snapping during move/resize/trim. |
snapTargets | list | ['clips','playhead','markers'] | Snap-eligible line categories: 'playhead', 'grid', 'markers', 'clips'. |
snapThreshold | number | 8 | Snap activation distance in screen px. |
gridFrames | number | 0 | Grid spacing in frames for 'grid' snapping. 0 = off. |
allowCrossTrackMove | boolean | True | Allow dragging a clip onto another lane of the same kind. |
multiSelect | boolean | True | Allow shift/cmd-click multi-selection. |
keyboard | boolean | True | Enable all keyboard shortcuts. |
readOnly | boolean | False | View-only: disable editing (scrub + selection + zoom still work). |
controls | boolean | True | Show the built-in transport bar above the ruler. |
theme | string | 'dark' | Chrome color theme: 'dark' or 'light'. |
headerWidth | number | 140 | Width of the left track-header column in px. |
rulerHeight | number | 28 | Height of the time-ruler row in px. |
className | string | - | CSS class name(s) for the root element. |
style | object | - | Inline CSS styles — give the timeline an explicit width/height. |
DashNleScene Props
| Property | Type | Default | Description |
|---|---|---|---|
id | string | - | Component ID for Dash callbacks. |
children | node | - | Background slot — e.g. a dash-leaflet2 Map. Rendered behind the tileset and overlay layers, filling the stage. |
playhead | number | - | Current playhead in integer frames (typically synced from a DashNleTimeline). Layers with a [start, end) window show/hide on it. |
playing | boolean | False | When True, video layers play natively; when False they seek to playhead / fps (frame-accurate scrub). Sync from the timeline. |
fps | number | 30 | Maps the playhead to seconds for video layers. |
layers | list of dicts | - | Overlay layers, bottom-to-top by z then array order. Keys: id*, kind ('image'/'video'/'text'), src, text, rect [x,y,w,h] (0..1), opacity, rotation, start, end, srcStart, z, color, fontSize. |
tileset | dict | - | Native XYZ tileset background. Keys: url, viewport ({center: [lat,lng], zoom}), tileSize (256), maxZoom (19), opacity (1), subdomains (['a','b','c']). |
camera | dict | - | Screen-space pan/zoom of the overlay layer group: {x: 0, y: 0, zoom: 1}. |
resolution | list | [1280, 720] | Output stage resolution [width, height] in px (sets the aspect ratio). |
background | string | '#000000' | Stage background CSS color (behind everything). |
className | string | - | CSS class name(s) for the root element. |
style | object | - | Inline CSS styles for the component. |
\ = required key inside the dict.*