Learn how to integrate IMG.LY’s video editor for React into your web app and make the most of all its features.
Short-form video changed what users expect from software. People who edit clips daily in TikTok or CapCut arrive at your product assuming they can trim, caption, and rearrange without leaving it. Sending them to a desktop tool and asking them to come back with an MP4 is where the workflow breaks.
This guide covers integrating a video editor into a React app with CreativeEditor SDK (CE.SDK): the component setup, the timeline and export APIs underneath it, and the customization points you will reach for once the default editor is running.
For a working reference, see the React video editor starter kit or try the interactive demo.
Updated September 2026.
Check browser support before you scope the work
CE.SDK’s video features encode and decode in the browser using the WebCodecs API. That is what lets you export an MP4 with no render farm behind it, and it is also the catch. Chrome and Edge 114 or newer support both video editing and export, and so does Safari 26 or newer. Firefox 130 or newer can edit video but cannot export it, because it does not ship the H.264 and AAC encoders the export needs. Chrome on Linux usually lacks those encoders as well, so exports can fail there. Chrome on Android and iOS and Safari on iOS do not support video editing yet, and other Chromium-based browsers such as Opera, Arc, and Brave may work but are not officially supported.
Design and photo editing work in more browsers, so this only limits the video path. If a meaningful share of your traffic is on Firefox, mobile, or older Safari versions, plan for a fallback message rather than discovering it in QA. cesdk.utils.supportsVideoDecode() and await cesdk.utils.supportsVideoEncode() let you check support in code, and the browser support page has the current detail.
Why a video editor belongs in the app rather than beside it
TikTok reported over a billion monthly active users in 2021. Products like it taught a very large number of people that trimming a clip and adding a caption takes seconds.
That expectation is why the round trip hurts. Every export-and-reupload cycle gives the user a chance to abandon the task, and every asset that leaves your system is one you can no longer template, version, or keep on brand. Editing in place keeps both the user and the asset.
React’s component model fits this well, because the editor is one mounted component with a configuration object and an initialization hook, not a page you navigate away to.
Getting started: adding a video editor in React
Requirements
- Node.js 22.12+. Download Node.js if needed.
- A React project on a build tool such as Vite, Parcel, or RSBuild.
- A CE.SDK license key, optional to start and needed for production. Start a free trial to get one.
If you do not have a project yet, create one with Vite:
npm create vite@latest my-react-video-editor -- --template react
cd my-react-video-editor
Step 1: Install CE.SDK
Install the SDK together with the background removal plugin that the editor configuration below uses:
npm install @cesdk/cesdk-js@1.82.0 @imgly/plugin-background-removal-web@1.82.0 onnxruntime-web@1.21.0
Step 2: Add the video editor configuration
The editor’s dock, inspector, timeline controls, and asset libraries come from an editor configuration. The React video editor starter kit ships it as source files, so copy them into your project:
npx degit imgly/starterkit-video-editor-ts-web/src/imgly#release-1.82.0 src/imgly
The release-1.82.0 tag matches the SDK version from Step 1. The configuration is written in TypeScript, which Vite handles even in a JavaScript project, and you change the editor by editing these files.
Step 3: Create the editor component
CE.SDK ships a React component, so you do not need to manage the instance lifecycle by hand. Import it from the /react entry point and pass it a config object and an init function.
Create src/VideoEditor.jsx:
// src/VideoEditor.jsx
import CreativeEditor from '@cesdk/cesdk-js/react';
import { initVideoEditor } from './imgly';
const config = {
userId: 'YOUR_USER_ID', // optional, for MAU tracking
};
// Runs once, after the SDK instance is created
const init = async (cesdk) => {
// Video editor UI, asset libraries and plugins from the starter kit
await initVideoEditor(cesdk);
// Open an empty video scene
await cesdk.actions.run('scene.create');
};
export default function VideoEditor() {
return (
<CreativeEditor
config={config}
init={init}
onError={(error) => console.error('[CE.SDK]', error.message, error.cause)}
width="100vw"
height="100vh"
/>
);
}
You do not need a CE.SDK license key to try it: without one the editor runs in evaluation mode and exports carry a watermark. Start a free trial to get one and set it as license in the config.
initVideoEditor applies the configuration and adds the asset libraries, including demo clips and music, upload sources, and background removal. In production you will swap the demo sources in src/imgly/index.ts for your own, which can be your backend or a third-party library such as Unsplash.
cesdk.actions.run('scene.create') opens an empty scene. Since CE.SDK 1.72 deprecated the design and video scene modes, every new scene supports video. Older examples that call createVideoScene() or addDemoAssetSources() still work, but both are deprecated. One caveat: the iOS and Android SDKs do not yet support scenes without a mode, so if you share scenes with mobile apps, read the v1.72 migration guide first.
The configuration also runs editor.checkBrowserSupport, which shows a blocking message in browsers that cannot decode video and a dismissible warning in browsers that cannot export it. Since CE.SDK 1.72, the editor no longer runs this check on its own, so keep that call if you replace the configuration.
The editor loads its engine assets from the IMG.LY CDN, which is meant for development. For production, serve the assets from your own servers.
Step 4: Mount it
// src/App.jsx
import VideoEditor from './VideoEditor';
export default function App() {
return <VideoEditor />;
}
The Vite starter styles in src/index.css give #root a fixed width and margins, which squeeze the editor. Replace the contents of that file with:
/* src/index.css */
body {
margin: 0;
}
Run npm run dev and open http://localhost:5173. You should get a full editor with a timeline: trimming, splitting, text overlays, audio tracks, filters, and transitions.
Name your own component something other than CreativeEditor. Reusing that name collides with the import and produces an Identifier has already been declared error.
Four things that will cost you an afternoon
These are the failure modes that come up most often in React integrations. None of them announce themselves clearly, which is what makes them expensive.
init errors go to onError
If any line inside the init callback throws, the component catches the error, skips the remaining lines, and passes the error to its onError prop. Without an onError handler, nothing appears in the console, and you get an editor that loads with a half-built scene and no explanation. That is why the component above logs errors through onError.
Symptoms to recognize: the editor renders, some blocks are missing, the console is clean, and the behavior changes depending on network timing.
Strict Mode creates the editor twice
In development, React Strict Mode mounts, unmounts, and remounts the component. The component starts creating the SDK on the first mount, disposes that instance once it has loaded and the component turns out to be unmounted, and only runs init for the second one. You will see the SDK load twice in development, but your init code runs once, for the editor you end up looking at.
Production builds create the editor once. There is nothing to fix here, only something to know.
Relative asset URLs resolve against the CDN
CE.SDK resolves relative URIs against config.baseURL, which points at the IMG.LY CDN. A path like /videos/intro.mp4 will not hit your own origin. Make local asset paths absolute before handing them to the engine:
const resolveUri = (uri) =>
uri.startsWith('/') ? `${window.location.origin}${uri}` : uri;
Trim calls throw on unloaded media
setTrimOffset and setTrimLength need the clip’s duration metadata. Call them before the resource has loaded and they throw The video has not been loaded yet. Inside init the component passes that error to onError and skips every line after it, so the trim never applies and the rest of the scene is never built. Always await engine.block.forceLoadAVResource(fill) first.
One more, if you are on Next.js: the editor is browser-only and will break server-side rendering. Load it with next/dynamic and ssr: false from a client component. If you hit ReferenceError: window is not defined, a 'use client' file alone has not been enough and the dynamic import is the fix.
Let an agent do the integration
If you work in an AI-assisted editor, you can skip most of the above. IMG.LY publishes Agent Skills for CE.SDK, which load CE.SDK’s documentation and conventions directly into Claude Code, Codex, and other agent tools. The agent then writes the integration against the current API instead of whatever it remembers from training.
There is also an MCP server that exposes the docs for live lookup during a session.
In practice this matters most for the parts that change between versions, such as component names and configuration shapes. We wrote about the reasoning behind it in Introducing IMG.LY Agent Skills.
Working with the timeline in code
The default UI covers most user-facing editing. When you need to build compositions programmatically, for templates, automation, or a custom UI, you work with the engine directly through cesdk.engine.
Create a video scene and a page
A page owns a timeline. Its duration comes from the clips you put on it, and that is the duration of the exported video.
const engine = cesdk.engine;
const scene = engine.scene.create();
const page = engine.block.create('page');
engine.block.appendChild(scene, page);
engine.block.setWidth(page, 1280);
engine.block.setHeight(page, 720);
Add video clips
Video is not its own block type. You create a graphic block and give it a video fill, which means the same positioning, cropping, and animation APIs apply to video as to any other element.
const video1 = engine.block.create('graphic');
engine.block.setShape(video1, engine.block.createShape('rect'));
const fill1 = engine.block.createFill('video');
engine.block.setString(
fill1,
'fill/video/fileURI',
'https://cdn.img.ly/assets/demo/v4/ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4'
);
engine.block.setFill(video1, fill1);
const video2 = engine.block.create('graphic');
engine.block.setShape(video2, engine.block.createShape('rect'));
const fill2 = engine.block.createFill('video');
engine.block.setString(
fill2,
'fill/video/fileURI',
'https://cdn.img.ly/assets/demo/v4/ly.img.video/videos/pexels-taryn-elliott-7108793.mp4'
);
engine.block.setFill(video2, fill2);
Sequence clips with a track
You could position each clip by hand with time offsets, but the background track does it for you. It keeps its children playing back to back with no gaps, recalculates the offsets whenever a duration changes, and sets the page duration. The timeline shows every track on the page, including ones you create yourself.
engine.block.moveToBackgroundTrack(video1);
const track = engine.block.getBackgroundTrack();
if (track === null) throw new Error('No background track');
// Set the duration before the next clip joins, so the track shifts video2
engine.block.setDuration(video1, 10);
engine.block.moveToBackgroundTrack(video2);
engine.block.fillParent(track); // size children to the page
Do not call setWidth or setHeight on a track. A track derives its dimensions from its children, so size the children or use fillParent. moveToBackgroundTrack creates the background track the first time you call it.
Trim clips
Blocks default to 5 seconds. If a clip is longer than its block it gets cut off; if shorter, it stops at its end, because looping is off until you turn it on. To control which portion plays, set a trim offset and length on the fill, after the resource has loaded.
await engine.block.forceLoadAVResource(fill1);
engine.block.setTrimOffset(fill1, 1); // skip the first second
engine.block.setTrimLength(fill1, 10); // play 10 seconds
engine.block.setLooping(fill1, true);
The forceLoadAVResource call is required. Trim APIs need the duration metadata, and calling them on an unloaded resource throws The video has not been loaded yet.
Add audio
A video fill plays its own audio track by default, which you can mute. Separate audio blocks handle music and voiceover.
engine.block.setMuted(fill1, true);
const audio = engine.block.create('audio');
engine.block.appendChild(page, audio);
engine.block.setString(
audio,
'audio/fileURI',
'https://cdn.img.ly/assets/demo/v4/ly.img.audio/audios/far_from_home.m4a'
);
engine.block.setVolume(audio, 0.7);
engine.block.setTimeOffset(audio, 2); // start 2s in
engine.block.setDuration(audio, 7);
Export to MP4
Export runs in a background worker, so the editor stays interactive while it encodes. The scene state is frozen when the export starts, so later edits will not appear in the file.
const exportPage = engine.scene.getCurrentPage();
if (!exportPage) throw new Error('No page to export');
const blob = await engine.block.exportVideo(exportPage, {
mimeType: 'video/mp4',
videoBitrate: 'Auto', // bounded bitrate, recommended in the browser
onProgress: (rendered, encoded, total) => {
setProgress(Math.round((encoded / total) * 100));
},
});
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = 'export.mp4';
anchor.click();
URL.revokeObjectURL(url);
Declare const [progress, setProgress] = useState(0) with useState imported from react, in the component that runs the export rather than in the module-scope init, and you have a progress bar. Encoding a long composition takes time, and a silent wait is the fastest way to make users think the app has hung.
You can also pull just the audio with engine.block.exportAudio(page, { mimeType: 'audio/wav' }), which is still marked experimental in the SDK types, and is useful for transcription and caption pipelines.
Two features worth knowing about once the basics work: transitions between clips, and caption presets for styling subtitles consistently. Captions in particular tend to arrive as a request about a week after launch, because most social video is watched muted.
For the full API surface, see the create and edit videos guide and the timeline editor reference.
Customizing the editor
CE.SDK is not a fixed editor with a theme switch. The main customization points:
- Component order. Add, remove, and reorder UI elements with
setComponentOrder({ in: location }, order), where locations include'ly.img.dock','ly.img.canvas.menu','ly.img.inspector.bar','ly.img.navigation.bar', and'ly.img.canvas.bar', which also takes anatposition of'top'or'bottom'. - Theming. Use
cesdk.ui.setTheme()for light, dark, or system, or build a custom theme with CSS custom properties. See the theming demo. - Custom components. Register your own buttons, dropdowns, and inputs and place them alongside the built-ins, or replace built-ins entirely.
- Feature API. Show and hide functionality by context, for example hiding controls for a particular block type.
- Asset sources. Serve video, audio, and image libraries from your own backend, or connect a third-party source such as Unsplash.
- Localization. Override and extend every string in any language with full i18n support.
When a customization is worth reusing across projects, package it as a plugin. Background removal and vectorization ship as plugins built on the same API you would use.
Where a React video editor pays off
Social publishing. Templates, an audio library, and configurable animations let users produce platform-ready clips, then export at the right aspect ratio per destination.
Digital asset management. Teams adapt and repurpose approved footage inside a central system. Templates with placeholders and locked layers keep output on brand. CE.SDK’s roles and scopes control what users can edit, while accounts and access rules stay in your own backend, and the Feature API reflects them in the UI.
Marketing automation. Generate campaign variants from one template using text variables, and push template changes across every asset that uses it. This is where the headless engine matters more than the UI, and it is worth reading about automated video generation.
Sales outreach. Personalized video at volume, with localization for multi-market teams.
E-learning. Instructors record, trim, and caption in the browser, building a reusable template library rather than one-off files.
Next steps
The fastest path from here is the video editor starter kit, which is a working project rather than a snippet. If you would rather see it running first, the demo is the same editor this guide produces.
Building the same thing on another framework? We have equivalent guides for Vue.js and Angular, and a broader JavaScript video editing guide covering the underlying browser technologies. If you are still comparing options rather than writing code, we put together a breakdown of the leading video editing SDKs.
Questions about a specific use case? Talk to us.
FAQ
Do I need a license key to try CE.SDK? No. Without one the editor runs in evaluation mode and exports carry a watermark. Start a free trial to get a key and set it as license in the config.
Is createVideoScene() still supported? It still works, but it has been deprecated since CE.SDK 1.72, which also deprecated the design and video scene modes. Use cesdk.actions.run(‘scene.create’) instead, which creates a scene that supports video by default.
Why does init fail without an error message? The React component catches errors thrown inside init and passes them to its onError prop. If you do not pass onError, the error never reaches the console, so add a handler that logs it.
Can I use CE.SDK with Next.js? Yes, as long as the editor only renders in the browser. The CE.SDK plugins need browser APIs, so if server rendering trips over them, load your editor component with next/dynamic and ssr: false from a file marked ‘use client’.
Can CE.SDK export MP4 without a server? Yes. In browsers that support video export, CE.SDK encodes the MP4 in the browser, so no render server is needed. For environments without that support, CE.SDK Renderer can render video on a server.


