This guide adds a complete video editor (multi-track timeline, preview, text, audio mixer, MP4 and WebM export) to a React app. Everything runs in the user’s browser, including the export, so there is no render server to set up. It uses Cutforge, which runs as a watermarked demo until you add a license key, so you can follow along for free.
Using Next.js? Follow the Next.js guide instead; it covers server rendering and headers.
1. Install
npm install @cutforge/editor react react-dom
React 18.3 or newer is required.
2. Mount the editor
import { Editor } from '@cutforge/editor';
import '@cutforge/editor/style.css';
export default function VideoEditor() {
return <Editor style={{ height: '100vh' }} />;
}
The editor fills its parent, so give it a real height. Without a key it runs in demo mode: exports carry a watermark and stop at 60 seconds and 720p, and saving is off.
3. Add your license key
// Vite exposes VITE_* variables; use your bundler's equivalent otherwise.
<Editor license={import.meta.env.VITE_CUTFORGE_LICENSE} style={{ height: '100vh' }} />
The key arrives by email after checkout. It ships to the browser like any client-side SDK key, so an environment variable is about keeping it out of your repository, not about secrecy.
4. Save and restore projects
The editor hands you its project on every edit. Keep it in state and store it as JSON wherever you like.
import { useState } from 'react';
import { Editor, type CutforgeProject } from '@cutforge/editor';
export default function VideoEditor({ initial }: { initial?: CutforgeProject }) {
const [project, setProject] = useState<CutforgeProject | undefined>(initial);
return (
<Editor
project={project}
onProjectChange={(p) => {
setProject(p);
localStorage.setItem('draft', JSON.stringify(p));
}}
style={{ height: '100vh' }}
/>
);
}
onProjectChange fires on edits, not on playback or selection, so it is cheap to persist.
5. Export and upload the video
The export renders on the user’s machine and arrives as a Blob. Send it to your own storage, for example with a presigned URL from S3, R2 or GCS:
async function upload(blob: Blob, filename: string) {
const res = await fetch(`/api/upload-url?name=${encodeURIComponent(filename)}`);
const { url } = await res.json();
await fetch(url, { method: 'PUT', body: blob, headers: { 'Content-Type': blob.type } });
}
<Editor onExportComplete={({ blob, filename }) => upload(blob, filename)} style={{ height: '100vh' }} />;
To start an export from your own button instead, take a ref to the editor’s commands:
import { useRef } from 'react';
import { Editor, type CutforgeCommands } from '@cutforge/editor';
const editor = useRef<CutforgeCommands>(null);
// ...
<Editor ref={editor} />
<button onClick={async () => upload((await editor.current!.export({ format: 'mp4' })).blob, 'video.mp4')}>
Publish
</button>
6. Turn on the fast audio path
The editor’s low-latency audio engine needs the page to be cross-origin isolated. Without it, the editor still works and quietly uses a simpler audio path. With Vite, add the headers in development:
// vite.config.ts
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
});
In production, send the same two headers from your host for the page that holds the editor.
Next steps
- Theme it, brand it, localize it, or plug in your own media library: see the docs.
- Compare it with other options: video editor SDKs compared.