React Video Editor
A modular, drop-in React video editor with sidebar panels, Remotion-powered player, renderer abstraction, autosave, theming, and pluggable media adaptors
The RVE Editor is a composable, drop-in editor shell that wires together the core building blocks of a video editing UI: a Remotion-powered player, a modular sidebar with overlay panels, timeline configuration, autosave hooks, theme management, and a renderer abstraction. You stay in control of data and backends via explicit props and adaptors.
Under the hood, RVE uses Remotion for rendering and playback, and exposes a renderer interface (e.g. HTTP/SSR or Lambda) so you can plug in your own backend. Media sourcing is handled by a flexible adaptor layer (e.g. Pexels), while overlays and timeline config remain app-controlled.
Editor vs Player-only
Use full Editor mode for an all-in-one editing UI (sidebar + editor + autosave). Switch to player-only mode when you just need an embeddable, fullscreen player with overlays and responsive sizing, for example on mobile or preview pages.
Key Features
- Renderer abstraction: Bring your own backend via a simple
VideoRendererinterface; ships withHttpRenderer. - Player-only mode: Fullscreen, responsive player without the editor UI for lightweight embeds.
- Modular sidebar: Default sidebar with pluggable overlay panels; disable or replace with your own.
- Autosave hooks:
onSavecallback delivers the full serializedEditorStateon every save, plus visible autosave status. - Theming: Custom theme registry, default themes toggle, and external theme switching.
- Media adaptors: Plug-in sources for video, images, audio, stickers, templates, and animations.
- Timeline config: Control rows, zoom constraints, snapping behavior, and push-on-drag.
- Responsive video sizing: Aspect ratio and explicit dimension controls for predictable layout.
- Mobile-conscious: Player-only mode includes mobile viewport height handling.
Usage Example
Basic (Client-Side Rendering — Default)
import { ReactVideoEditor } from '@reactvideoeditor/react-video-editor/react-video-editor';
import '@reactvideoeditor/react-video-editor/styles.css';
export default function VideoEditorPage() {
return (
<div className="w-full h-screen">
<ReactVideoEditor
projectId="MyComposition"
fps={30}
disabledPanels={[]}
defaultTheme="dark"
showDefaultThemes={true}
/>
</div>
);
}With Server Rendering (Optional)
import React from 'react';
import { HttpRenderer } from '@reactvideoeditor/react-video-editor/utils/http-renderer';
import { ReactVideoEditor } from '@reactvideoeditor/react-video-editor/react-video-editor';
import { createPexelsVideoAdaptor } from '@reactvideoeditor/react-video-editor/adaptors/pexels-video-adaptor';
import { createPexelsImageAdaptor } from '@reactvideoeditor/react-video-editor/adaptors/pexels-image-adaptor';
import '@reactvideoeditor/react-video-editor/styles.css';
export default function VideoEditorPage() {
const PROJECT_ID = 'MyComposition';
const availableThemes = [
{ id: 'rve', name: 'RVE', className: 'rve', color: '#3E8AF5' },
];
const ssrRenderer = React.useMemo(
() =>
new HttpRenderer('/api/latest/ssr', {
type: 'ssr',
entryPoint: '/api/latest/ssr',
}),
[]
);
return (
<div className="w-full h-screen">
<ReactVideoEditor
projectId={PROJECT_ID}
fps={30}
customRenderer={ssrRenderer}
enableWebRender={true}
disabledPanels={[]}
availableThemes={availableThemes}
defaultTheme="dark"
sidebarWidth="400px"
sidebarIconWidth="57.6px"
showIconTitles={false}
adaptors={{
video: [createPexelsVideoAdaptor('YOUR_PEXELS_API_KEY')],
images: [createPexelsImageAdaptor('YOUR_PEXELS_API_KEY')],
}}
onThemeChange={(themeId) => console.log('Theme changed:', themeId)}
/>
</div>
);
}Component Structure
ReactVideoEditor/
├── components/
│ ├── core/
│ │ ├── editor.tsx # Main editor surface
│ │ └── video-player.tsx # Remotion-based player
│ ├── shared/
│ │ └── default-sidebar.tsx # Default sidebar with overlay panels
│ ├── autosave/
│ │ └── autosave-status.tsx # Autosave UI indicator
│ ├── providers/
│ │ ├── react-video-editor-provider.tsx # Top-level provider composition
│ │ └── editor-provider.tsx # Core editor context wiring
│ └── ui/ # Sidebar/tooltip/button primitives
├── contexts/
│ ├── editor-context.tsx
│ ├── renderer-context.tsx
│ ├── media-adaptor-context.tsx
│ └── sidebar-context.tsx
├── hooks/
│ ├── use-overlays.tsx
│ ├── use-render.ts
│ └── use-extended-theme-switcher.ts
├── utils/
│ └── http-renderer.ts # HTTP renderer implementing VideoRenderer
├── types/
│ ├── renderer.ts # VideoRenderer interface
│ ├── overlay-adaptors.ts # Overlay adaptor interfaces
│ └── index.ts # OverlayType, Overlay, etc.
└── constants.ts # DEFAULT_OVERLAYS, colors, etc.Props
The ReactVideoEditor component extends the editor provider props and adds UI-level controls. Required props are marked with *.
| Prop | Type | Default | Description |
|---|---|---|---|
| Core | |||
projectId* | string | - | Composition/session ID used for renders and state |
renderer | VideoRenderer | - | Deprecated — use customRenderer instead |
fps | number | 30 | Frames per second for playback and renders |
editorState | EditorState | - | Initial editor state. Pass the object from onSave to restore a previous session |
| Autosave | |||
autoSaveInterval | number | 10000 | Autosave interval in milliseconds |
onSave | (state: EditorState) => void | - | Called on every save (autosave or manual). Receives the full serialized editor state — use this to persist to your backend |
| Sidebar and Layout | |||
showSidebar | boolean | true | Toggle the default sidebar |
customSidebar | ReactNode | - | Provide your own sidebar; hides the default |
sidebarLogo | ReactNode | - | Custom logo for the default sidebar |
sidebarFooterText | string | - | Footer text in the default sidebar |
disabledPanels | OverlayType[] | - | Hide specific overlay panels |
showIconTitles | boolean | true | Show/hide sidebar icon titles |
sidebarWidth | string | "16rem" | CSS width var for content sidebar |
sidebarIconWidth | string | "3rem" | CSS width var for icon rail |
className | string | - | Applied to main content inset |
| Player / Mode | |||
isPlayerOnly | boolean | false | Fullscreen player without editor UI |
isLoadingProject | boolean | false | Whether the project from URL is still loading |
| Renderer & API | |||
customRenderer | VideoRenderer | - | Optional cloud/server renderer (SSR, Lambda, or custom). Not required — the SDK uses client-side rendering by default |
enableWebRender | boolean | true if no customRenderer | Enable client-side video export via WebCodecs. Enabled by default when no customRenderer is provided |
exportOptions | ExportOptions | { availableResolutions: ['720p', '1080p', '4k'], defaultResolution: '1080p' } | Configure the resolution presets shown in the export dialog. See Export Options |
disableExportModal | boolean | false | Skip the resolution dialog. The Render button exports immediately at the exact composition size. See Composition Dimensions |
defaultExportScale | number | - | Render multiplier applied by the built-in Render button, so you can edit on a small logical canvas but export at native resolution (composition size × scale). Only applies on the disableExportModal immediate-render path. See Composition Dimensions |
baseUrl | string | - | Optional API base for editor operations |
| Media Adaptors | |||
adaptors | OverlayAdaptors | - | Pluggable sources for content (video, images, audio, text, stickers, templates, animations) |
mediaLibrary | MediaLibraryConfig | - | Configure the in-panel media library tabs (Stock / My Library): which tabs show, their order, and their labels. Omit to derive tabs from configured adaptors. See Configuring the library tabs |
| Timeline | |||
initialRows | number | 5 | Initial number of timeline rows |
maxRows | number | 8 | Maximum number of timeline rows |
| Zoom | |||
zoomConstraints | object | { min: 0.2, max: 10, step: 0.1, default: 1 } | Zoom level constraints |
| Snapping | |||
snappingConfig | object | { thresholdFrames: 1, enableVerticalSnapping: true } | Timeline snapping configuration |
| Feature Flags | |||
disableMobileLayout | boolean | false | Disable mobile-specific layout |
disableVideoKeyframes | boolean | false | Disable video keyframe functionality |
enablePushOnDrag | boolean | false | Enable push-on-drag timeline behavior |
| Video Dimensions | |||
videoWidth | number | 1280 | Video output width |
videoHeight | number | 720 | Video output height |
| Theming | |||
availableThemes | CustomTheme[] | - | List of custom themes |
selectedTheme | string | undefined | - | Active theme id |
onThemeChange | (themeId: string) => void | - | Theme change callback |
showDefaultThemes | boolean | true | Show/hide default themes |
hideThemeToggle | boolean | false | Hide theme toggle UI |
defaultTheme | string | "dark" | Default theme to use |
| Watermark | |||
watermark | WatermarkConfig | true | Watermark on your videos. true for default RVE badge, { src: '...' } for custom image, false for none. See Watermark |
| Status UI | |||
showAutosaveStatus | boolean | true | Show autosave indicator |
Notes
- The SDK ships with client-side rendering enabled by default. You do not need to provide a renderer to get video export working.
- The component internally manages a
playerRefand wires it into theVideoPlayer. If you need direct player access, read it from the editor context or adapt the provider usage directly. - Pass a previously-saved
EditorStatevia theeditorStateprop to seed the editor with initial content. - The
HttpRendereris a convenience implementation ofVideoRendererthat expects REST endpoints at/renderand/progress. It is only needed if you want server-side rendering.
Export Options
The exportOptions prop controls the resolution presets shown in the export dialog. When a user clicks "Export", they see a dialog to choose a resolution before rendering begins. The pixel dimensions are calculated automatically based on the current aspect ratio.
<ReactVideoEditor
projectId="my-project"
exportOptions={{
availableResolutions: ['720p', '1080p', '4k'],
defaultResolution: '1080p',
}}
/>| Property | Type | Default | Description |
|---|---|---|---|
availableResolutions | ('720p' | '1080p' | '4k')[] | ['720p', '1080p', '4k'] | Resolution presets offered in the export dialog |
defaultResolution | '720p' | '1080p' | '4k' | '1080p' | Resolution selected when the dialog opens |
The resolutions map to the short edge of the output — for a 16:9 aspect ratio, 1080p produces 1920×1080, while for 9:16 it produces 608×1080. The dialog shows the calculated pixel dimensions for each option.
Composition Dimensions
Set the canvas to an exact pixel size at any ratio via editorState.dimensions. The same size is used in both preview and export.
<ReactVideoEditor
projectId="my-project"
editorState={{ tracks, dimensions: { width: 593, height: 4395 } }}
disableExportModal
/>How it works:
- Size is the source of truth. You pass
dimensions; the aspect-ratio label is derived from it. The ratio dropdown shows a matching preset (16:9, 1:1, …) when one fits, otherwise the raw dimensions (e.g.594 × 4396). Picking a preset just sets a new size and reframes items. - Omit
dimensionsand the canvas defaults to 16:9. - Export at the exact size. With a custom size, the export dialog adds an "Original" option (the composition size) and selects it by default. It's hidden when a preset already produces that size, so standard sizes like 1920×1080 don't show a duplicate row.
disableExportModalskips the dialog entirely — the Render button exports immediately at "Original" (the composition size), no scaling. Use it when the canvas size is the output size.defaultExportScalelets that immediate-render path export larger than the canvas. The Render button renders at composition size × the scale, so you can edit on a small logical canvas but export at native resolution (e.g. preview 1332×720,defaultExportScale={2.88}→ ~3840×2076). It only applies together withdisableExportModal— the resolution dialog has its own presets and ignores it, so setting the prop withoutdisableExportModalis a no-op.- Persistence.
dimensionsis saved inEditorState, so custom sizes survive reload. Pass the object back to restore.
<ReactVideoEditor
projectId="my-project"
editorState={{ tracks, dimensions: { width: 1332, height: 720 } }}
disableExportModal
defaultExportScale={2.88}
/>SSR / Lambda renders
For cloud rendering, your app's Remotion <Composition> must read width/height from inputProps for arbitrary sizes to reach the render. Client-side (WebCodecs) export uses the composition size directly.
EditorState In, EditorState Out
The onSave callback emits an EditorState on every autosave tick and manual save. Pass that same object back via the editorState prop to restore a session — no destructuring or field mapping required.
import { ReactVideoEditor } from '@reactvideoeditor/react-video-editor/react-video-editor';
import type { EditorState } from '@reactvideoeditor/react-video-editor/types';
function Editor({ savedState }: { savedState?: EditorState }) {
const handleSave = (state: EditorState) => {
// Persist to your backend
fetch('/api/projects/my-project', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state),
});
};
return (
<ReactVideoEditor
projectId="my-project"
editorState={savedState}
onSave={handleSave}
/>
);
}The editor uses timestamp-based conflict resolution to decide whether to restore from local IndexedDB or use the provided editorState:
editorStatewithsavedAt— compared against the local autosave timestamp. Whichever is newer wins. Local changes made after the last server save are preserved; fresh server data takes priority over stale local state.editorStatewithoutsavedAt— IndexedDB is always restored if a local save exists. The prop is treated as a fallback for when no local state is available.editorStateomitted — falls back to IndexedDB or internal defaults.
The savedAt field is stamped automatically on every save and included in the onSave payload — persist it alongside the rest of the state and pass it back.
The EditorState type contains source-of-truth fields only (no derived values):
interface EditorState {
tracks: Track[];
dimensions?: { width: number; height: number }; // Canvas size at any ratio. Omit → 16:9 default. See Composition Dimensions
aspectRatio?: AspectRatio; // Deprecated — prefer `dimensions`. '16:9' | '1:1' | '4:5' | '9:16' | 'custom'
backgroundColor: string;
playbackRate: number;
savedAt?: number; // Unix-ms timestamp, set automatically on every save
}aspectRatio is deprecated
aspectRatio is now optional and deprecated in favor of dimensions. State that sets only aspectRatio still works — the size is seeded from the preset — so no changes are required for existing consumers. See Composition Dimensions.
Need pixel dimensions for an aspect ratio? Use the exported getCanvasDimensions utility:
import { getCanvasDimensions } from '@reactvideoeditor/react-video-editor/types';
const { width, height } = getCanvasDimensions('16:9');
// → { width: 1920, height: 1080 }OPFS asset URLs
Asset URLs from the browser's Origin Private File System (OPFS) are dehydrated to asset:{id} references in the state. If your workflow uses external URLs (e.g. https://), they pass through as-is. OPFS-based assets will need a URL resolution strategy on your backend — this is a separate concern from state persistence.
Deprecated: onSaving / onSaved
The onSaving and onSaved props have been removed. Use onSave instead — it provides the full EditorState on every save, replacing both callbacks with a single, more useful one.
Next Steps
- Get started with the default sidebar panels and overlays — client-side export works out of the box.
- Optionally wire up a server renderer (SSR or Lambda) via
customRendererfor production workloads. - Add media sources via
adaptors(e.g. Pexels for images/videos). - Customize themes and integrate your app's theme switching UX.