Quickstart
Minimal working editor, then add plugins, then load a real project.
This page is three escalating examples: a smoke-test editor with the defaults, the same editor with two official plugins wired up, and the production pattern for loading a project asynchronously.
1. Minimal editor
The smallest amount of code that gets you a working editor on screen. The provider creates an empty 1920×1080 @ 30fps project under the hood — you can add tracks and items immediately.
'use client'
import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'
export default function Editor() {
return (
<VideoStudioProvider theme="dark">
<StudioEditor className="h-screen" />
</VideoStudioProvider>
)
}That's it. No callbacks, no plugins, no bundle. The editor mounts, the toolbar/timeline/inspector render, hotkeys work (Space, Cmd+Z, Delete, …). Save and Export buttons appear but do nothing yet — the next sections wire them up.
2. With plugins
Plugins are installed independently and registered via the plugins prop. Each plugin's factory
takes optional config — most have sensible defaults.
'use client'
import { useMemo } from 'react'
import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import { filtersPlugin } from '@studio-dev/vsdk-plugin-filters'
import { typographyPlugin } from '@studio-dev/vsdk-plugin-typography'
import { gifPlugin, createGifAdapter } from '@studio-dev/vsdk-plugin-gif'
import {
stockAssetPlugin,
createStockAssetAdapter,
} from '@studio-dev/vsdk-plugin-stock'
import '@studio-dev/vsdk/styles.css'
export default function Editor() {
// Memoize: plugin factories build a fresh definition each call.
const plugins = useMemo(
() => [
filtersPlugin(),
typographyPlugin(),
gifPlugin({
adapter: createGifAdapter({
giphy: { apiKey: process.env.NEXT_PUBLIC_GIPHY_KEY! },
}),
}),
stockAssetPlugin({
adapter: createStockAssetAdapter({
pexels: { apiKey: process.env.NEXT_PUBLIC_PEXELS_KEY!, mediaType: 'mixed' },
freesound: { apiKey: process.env.NEXT_PUBLIC_FREESOUND_KEY!, minDuration: 10 },
}),
}),
],
[],
)
return (
<VideoStudioProvider plugins={plugins} theme="dark" defaultActivePanel="filters">
<StudioEditor className="h-screen" />
</VideoStudioProvider>
)
}Each plugin contributes one or more sidebar panels and (for filters, typography) inspector sections — they appear automatically without any extra wiring on your side.
3. Loading a project asynchronously
The provider accepts an initialBundle synchronously, but real apps fetch projects by ID. Two
patterns are common:
Pattern A — fetch outside, mount with the bundle
Suspense or a top-level loading state on your page, then render the provider once you have a bundle:
import { Editor } from './editor'
async function loadProject(id: string) {
const res = await fetch(`${process.env.API_URL}/projects/${id}`, {
next: { revalidate: 0 },
})
return res.json()
}
export default async function Page({ params }: { params: { id: string } }) {
const bundle = await loadProject(params.id)
return <Editor bundle={bundle} />
}'use client'
import { StudioEditor, VideoStudioProvider, type ProjectBundle } from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'
export function Editor({ bundle }: { bundle: ProjectBundle }) {
return (
<VideoStudioProvider initialBundle={bundle}>
<StudioEditor className="h-screen" />
</VideoStudioProvider>
)
}Pattern B — mount empty, load with setLoading overlay
When the bundle isn't ready at mount time, use the store's setLoading action to show a blurred
loading overlay over the editor:
'use client'
import { useEffect } from 'react'
import {
StudioEditor,
VideoStudioProvider,
useStudioStoreApi,
} from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'
function ProjectLoader({ projectId }: { projectId: string }) {
const storeApi = useStudioStoreApi()
useEffect(() => {
storeApi.getState().setLoading(true)
fetch(`/api/projects/${projectId}`)
.then((r) => r.json())
.then((bundle) => storeApi.getState().loadBundle(bundle))
.finally(() => storeApi.getState().setLoading(false))
}, [projectId, storeApi])
return null
}
export default function Editor({ projectId }: { projectId: string }) {
return (
<VideoStudioProvider>
<ProjectLoader projectId={projectId} />
<StudioEditor className="h-screen" />
</VideoStudioProvider>
)
}The setLoading(true) overlay covers the whole editor with a spinner. setLoading(false) removes it.
Calling loadBundle resets selection, history, and playback — it's a hard load.
Saving and exporting
Wire onSave and onExport once you have a backend:
<VideoStudioProvider
initialBundle={bundle}
onSave={async (b) => {
await fetch(`/api/projects/${b.metadata.id}`, {
method: 'PUT',
body: JSON.stringify(b),
})
}}
onExport={async (payload) => {
await fetch('/api/renders', { method: 'POST', body: JSON.stringify(payload) })
// Drive the SDK's export UI via store actions as your render job progresses
// (setExportProgress / setExportCompleted / setExportFailed).
}}
>See Configuration for the full callback signatures.
What's next
Color system
Override theme tokens, per-mode overrides, the full token map.
Icon system
Replace icons globally, look icons up at runtime, register plugin icons.
Store
The full state shape, every action, and how to read/write it from your code.
Plugin authoring
Three skill levels, every extension point, common recipes.
Configuration
Every prop on VideoStudioProvider, explained — what it does, when to set it, the type shape, defaults, gotchas, and worked recipes for each.
Core overview
How the editor is put together — the bundle that is the source of truth, the store that holds state, the commands that mutate it, the timeline, and the event bus.