Examples
Integration examples for common use cases. Copy, adapt, ship.
Complete production example
Runnable, end-to-end editor apps consuming the published @elah/editor package. Clone, install, and run — or read the source on GitHub.
React
A complete production editor — preview, timeline, asset/element panels, text inspector, and MP4 export — wired as a standalone Vite app consuming @elah/editor from npm.
View on GitHubNext.js
The same full editor composition in a Next.js App Router app, with the client-only dynamic import and transpilePackages config needed to ship it in production.
View on GitHubTimeline Only
The timeline as a drop-in component — no renderer, no decode pipeline. Ideal for building a custom rendering layer or exploring the edit model.
1import { useRef } from 'react'2import {3 EditorProvider,4 Timeline,5 useTimelineEngine,6 usePlaybackStore,7 framesToTimecode,8 type TimelineRef,9} from '@elah/editor'1011const TRACKS = [12 { kind: 'video' as const, name: 'Video / Image' },13 { kind: 'audio' as const, name: 'Audio' },14 { kind: 'text' as const, name: 'Text' },15]1617function Transport({ fps }: { fps: number }) {18 const { isPlaying, togglePlayPause, currentFrame } =19 usePlaybackStore((s) => s)2021 return (22 <div style={{ display: 'flex', gap: 8, padding: 8 }}>23 <button onClick={togglePlayPause}>24 {isPlaying ? '⏸' : '▶'}25 </button>26 <span style={{ fontFamily: 'monospace', fontSize: 12 }}>27 {framesToTimecode(currentFrame, fps)}28 </span>29 </div>30 )31}3233export default function TimelineOnlyExample() {34 const ref = useRef<TimelineRef>(null)35 const fps = 303637 return (38 <EditorProvider fps={fps} initialTracks={TRACKS}>39 <Transport fps={fps} />40 <Timeline ref={ref} fps={fps} style={{ height: 260 }} />41 </EditorProvider>42 )43}Custom Demuxer
Replace mediabunny with your own demuxer by implementing the DemuxerFactory interface.
1import {2 EditorProvider,3 Preview,4 Timeline,5 type DemuxerFactory,6} from '@elah/editor'78// Implement DemuxerFactory with your own backend9const myDemuxerFactory: DemuxerFactory = () => ({10 async probe(src: string) {11 const res = await fetch(`/api/probe?src=${encodeURIComponent(src)}`)12 return res.json() // { width, height, durationSeconds, fps }13 },14 async demux(src, opts, onChunk) {15 const res = await fetch(src)16 const reader = res.body!.getReader()17 // Feed chunks to your WebCodecs VideoDecoder18 while (true) {19 const { done, value } = await reader.read()20 if (done) break21 onChunk(new EncodedVideoChunk({22 type: 'key',23 timestamp: opts.startTimestamp,24 data: value,25 }))26 }27 },28 destroy() {},29})3031export default function CustomDemuxerExample() {32 return (33 <EditorProvider fps={30}>34 <Preview demuxerFactory={myDemuxerFactory} style={{ flex: 1 }} />35 <Timeline fps={30} style={{ height: 240 }} />36 </EditorProvider>37 )38}Export with Progress UI
Full export flow with progress bar, cancel support, and MP4 download trigger.
1import { useState, useCallback } from 'react'2import {3 exportVideo,4 useTimelineEngine,5 createDefaultDemuxerFactory,6 type ExportProgress,7} from '@elah/editor'89const demuxerFactory = createDefaultDemuxerFactory()1011export function ExportPanel() {12 const engine = useTimelineEngine()13 const [progress, setProgress] = useState<ExportProgress | null>(null)14 const [error, setError] = useState<string | null>(null)1516 const handleExport = useCallback(async () => {17 setError(null)18 try {19 const blob = await exportVideo(engine.getProject(), {20 fps: 30,21 demuxerFactory,22 videoCodec: 'avc',23 videoBitrate: 8_000_000,24 onProgress: setProgress,25 })2627 // Trigger download28 const a = Object.assign(document.createElement('a'), {29 href: URL.createObjectURL(blob),30 download: 'export.mp4',31 })32 a.click()33 URL.revokeObjectURL(a.href)34 } catch (e) {35 setError(e instanceof Error ? e.message : 'Export failed')36 } finally {37 setProgress(null)38 }39 }, [engine])4041 return (42 <div style={{ padding: 16 }}>43 <button44 onClick={handleExport}45 disabled={!!progress}46 style={{ padding: '8px 16px', background: '#b7102a', color: '#fff' }}47 >48 {progress ? `Exporting ${Math.round(progress.percent)}%` : 'Export MP4'}49 </button>5051 {progress && (52 <div style={{ marginTop: 8 }}>53 <progress value={progress.percent} max={100} style={{ width: '100%' }} />54 <div style={{ fontSize: 11, color: '#666', marginTop: 4 }}>55 {progress.phase} · frame {progress.frame} / {progress.totalFrames}56 </div>57 </div>58 )}5960 {error && (61 <div style={{ marginTop: 8, color: '#b7102a', fontSize: 12 }}>62 Error: {error}63 </div>64 )}65 </div>66 )67}Custom Renderer with resolveTimeline
Use the pure resolver to build a completely custom rendering layer — DOM, Canvas 2D, or any other target.
1import { useEffect, useRef } from 'react'2import {3 EditorProvider,4 Timeline,5 useTimelineEngine,6 usePlaybackStore,7 useTracksStore,8 resolveTimeline,9} from '@elah/editor'1011// DOM renderer: updates div positions instead of WebGL12function DomPreview() {13 const engine = useTimelineEngine()14 const currentFrame = usePlaybackStore((s) => s.currentFrame)15 const containerRef = useRef<HTMLDivElement>(null)1617 useEffect(() => {18 const project = engine.getProject()19 const scene = resolveTimeline(currentFrame, project)2021 if (!containerRef.current) return22 containerRef.current.innerHTML = ''2324 // Render text clips as DOM elements25 for (const text of scene.texts) {26 const el = document.createElement('div')27 el.textContent = text.text?.content ?? ''28 el.style.cssText = `29 position: absolute;30 opacity: ${text.opacity};31 font-size: ${text.text?.fontSize ?? 32}px;32 color: ${text.text?.color ?? '#fff'};33 left: 50%;34 top: 50%;35 transform: translate(-50%, -50%);36 `37 containerRef.current.appendChild(el)38 }39 }, [currentFrame, engine])4041 return (42 <div43 ref={containerRef}44 style={{45 position: 'relative',46 flex: 1,47 background: '#000',48 overflow: 'hidden',49 }}50 />51 )52}5354export default function CustomRendererExample() {55 return (56 <EditorProvider fps={30}>57 <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>58 <DomPreview />59 <Timeline fps={30} style={{ height: 240 }} />60 </div>61 </EditorProvider>62 )63}Check the API reference or open an issue on GitHub.
