Theming and customization
Brand the editor — colors, icons, logos, sidebar buttons, panel slots, and the entire studio token system. Recipes for every level of customization, from a one-line accent swap to a complete visual rebrand.
The SDK is designed to be branded without forking. Every visible surface — colors, icons, the
logo, sidebar slots, the toolbar — flows from a small set of overrides on <VideoStudioProvider>
and a token-driven CSS layer. This page is the end-to-end customization guide — recipes from
trivial to total visual rebrand.
This page is the how-to. The full reference for every token, its CSS variable name, and what surfaces consume it lives in Color system. Use both — this for recipes, that for the canonical token map.
The customization stack
The editor's appearance is determined by four overlapping layers, each more invasive than the last:
| Layer | What you change | Surface |
|---|---|---|
| 1. Provider props | One-line knobs — theme, logo, defaultActivePanel, sidebarButtons, icons | The cosmetic top-layer of the editor |
| 2. Theme token overrides | Color tokens — accent, surfaces, borders, text, track colors | Every Tailwind utility / CSS-var-driven surface |
| 3. Plugin-contributed UI | Add panels / inspector sections / toolbar buttons | New, custom surfaces |
| 4. Tailwind utilities + custom CSS | Any pixel-level styling | Custom surfaces you render yourself |
Most apps live entirely at layers 1 and 2. Layer 3 is the Plugin authoring guide. Layer 4 is just "you have CSS" — covered briefly below.
Layer 1 — Provider knobs
These are one-line cosmetic settings on <VideoStudioProvider>. No theme math, no CSS.
theme
<VideoStudioProvider theme="light"> {/* always light */}
<VideoStudioProvider theme="dark"> {/* always dark */}
<VideoStudioProvider theme="system"> {/* default — follow OS preference */}The provider adds .dark to its root <div> when dark mode is active. Inside the editor,
Tailwind's dark:bg-… modifier works as expected — the SDK reads from data-theme and the .dark
class.
logo
<VideoStudioProvider logo={<img src="/brand.svg" className="h-5 w-auto" />}>
<VideoStudioProvider logo={<MyLogoComponent />}>
<VideoStudioProvider logo="Acme Studio"> {/* string — rendered as text */}
<VideoStudioProvider logo={null}> {/* default VideoIcon */}Accepts any ReactNode. The toolbar reserves a logo slot of ~24px height — keep your asset within
that.
sidebarButtons
App-level navigation, rendered at the bottom of the icon sidebar (desktop) and at the start of the mobile bottom bar.
import { LogOutIcon, SettingsIcon } from 'lucide-react'
const BUTTONS = [
{ id: 'settings', label: 'Settings', icon: SettingsIcon, onClick: () => openSettings() },
{ id: 'dashboard', label: 'Dashboard', icon: LogOutIcon, onClick: () => router.push('/') },
]
<VideoStudioProvider sidebarButtons={BUTTONS}>Use this for "back to my app" actions and account/settings entry points that live alongside the editor.
defaultActivePanel
<VideoStudioProvider defaultActivePanel="stock">The id of the plugin panel to open by default. Use this to make a specific plugin the user's first surface.
Layer 2 — Theme tokens
This is where the real branding lives. Three concentric ranges of tokens layer on top of each other:
studio primitives → studio semantic tokens → shadcn semantic tokens → components
(gray, accent) (surface, border, text) (background, primary)Override at any level and everything downstream updates.
Quickest possible recipe — change the accent
<VideoStudioProvider
themeOverrides={{
primary: '14 165 233', // sky-500, raw RGB triple
studioAccent: '14 165 233',
studioAccentHover: '2 132 199',
studioSelectionColor: '14 165 233',
}}
>Four tokens and the editor's primary buttons, focus ring, accent surfaces, and transform-handle color all shift together.
Values are raw RGB channel triplets ("R G B" with spaces, no rgb() wrapper, no #). This
is what lets Tailwind utilities like bg-studio-accent/30 apply alpha correctly.
Per-mode overrides (different values for light and dark)
<VideoStudioProvider
themeOverrides={{
light: {
primary: '14 116 144',
studioAccent: '14 116 144',
studioAppBg: '248 250 252',
},
dark: {
primary: '56 189 248',
studioAccent: '56 189 248',
studioAppBg: '8 11 19',
},
}}
>The provider picks the right bag based on prefers-color-scheme (or your theme prop).
Full visual rebrand recipe
Six categories of tokens cover everything you'd typically change for a brand-level rebrand:
<VideoStudioProvider
theme="dark"
themeOverrides={{
// 1. Accent (4 tokens) — drives buttons, focus ring, selection
primary: '124 58 237',
studioAccent: '124 58 237',
studioAccentHover: '109 40 217',
studioSelectionColor: '124 58 237',
// 2. Surface stack (5 tokens) — outermost bg to floating popovers
studioAppBg: '12 10 24',
studioSurfaceGround: '20 18 36',
studioSurfaceRaised: '28 26 48',
studioSurfaceOverlay: '38 36 62',
studioSurfaceFloat: '52 48 84',
// 3. Border tokens (3 tokens)
studioBorderSubtle: '36 32 56',
studioBorderDefault: '52 48 78',
studioBorderStrong: '78 72 110',
// 4. Text tokens (4 tokens)
studioTextPrimary: '243 244 246',
studioTextSecondary: '180 184 200',
studioTextTertiary: '130 136 160',
studioTextDisabled: '88 92 116',
// 5. Timeline (6 tokens)
studioTimelineBg: '14 12 28',
studioTimelineRulerBg: '18 16 36',
studioTimelineRulerText: '160 168 200',
studioTimelineTrackBorder: '36 32 56',
studioTimelinePlayhead: '244 63 94', // red — leave as is unless you want pink/orange
studioTimelineSnap: '250 204 21', // amber
// 6. Track type colours (9 tokens — usually you only change a couple)
studioTrackVideo: '99 102 241',
studioTrackAudio: '139 92 246',
studioTrackText: '236 72 153',
studioTrackImage: '14 165 233',
studioTrackOverlay: '20 184 166',
}}
>That's ~30 tokens. The result is a fully rebranded editor. For the complete token map see Color system.
Apply token overrides imperatively
For live theme editors (e.g. an in-app "appearance" page), apply tokens programmatically:
import { applyThemeTokensToElement, createThemeTokens } from '@studio-dev/vsdk'
function applyBrand(brand: 'sky' | 'violet') {
const root = document.querySelector('.vsdk-root') as HTMLElement
const tokens = createThemeTokens(
brand === 'sky'
? { primary: '14 165 233', studioAccent: '14 165 233' }
: { primary: '124 58 237', studioAccent: '124 58 237' }
)
applyThemeTokensToElement(root, tokens)
}applyThemeTokensToElement writes the tokens as inline style properties on the element, which
take precedence over the provider's static overrides.
Per-mode resolution helper
import { resolveThemeOverrides, type ThemeOverrides } from '@studio-dev/vsdk'
const overrides: ThemeOverrides = {
light: { primary: '30 80 50' },
dark: { primary: '155 120 255' },
}
const tokens = resolveThemeOverrides(overrides, 'dark') // { primary: '155 120 255' }resolveThemeOverrides is what the provider uses internally — exposed for advanced cases (custom
theme editors, multi-tenant theme switching).
Icons
Every icon in the editor resolves through a single map — StudioIconMap. Override one or many on
the provider.
Override one icon
import { Save } from 'lucide-react'
<VideoStudioProvider icons={{ save: Save }}>Override all icons (full icon pack swap)
import * as Lucide from 'lucide-react'
// Define OUTSIDE the component (or wrap in useMemo) — the SDK compares by identity.
const ICONS = {
save: Lucide.Save,
play: Lucide.Play,
pause: Lucide.Pause,
undo: Lucide.Undo2,
redo: Lucide.Redo2,
delete: Lucide.Trash2,
add: Lucide.Plus,
// …continue for every key in StudioIconMap
}
<VideoStudioProvider icons={ICONS}>Override precedence
defaults < plugin-registered icons (registerIcons) < user overrides (icons prop)The user always wins. A plugin registering stockPhoto won't override your icons={{ stockPhoto: … }}.
For the full icon-key list and the override patterns, see Icon system.
Tailwind utilities in your own UI
The SDK registers a Tailwind v4 @theme inline block that exposes the studio tokens as utilities.
Mix them into your own components so they match the editor:
<div className="bg-studio-surface-raised text-studio-text-primary border border-studio-border-subtle rounded-lg p-4">
<span className="text-studio-track-audio">Audio</span>
<div className="bg-studio-accent/30">Soft accent surface</div>
</div>
<button className="bg-studio-accent text-white hover:bg-studio-accent/90 rounded-md px-3 py-1.5">
Brand-matched button
</button>Available utility prefixes:
bg-studio-{app-bg, surface-ground, surface-raised, surface-overlay, surface-float}bg-studio-{accent, accent-hover, accent-subtle, selection-color}bg-studio-{track-video, track-audio, track-text, track-image, track-overlay, track-shape, track-captions, track-effect, track-adjustment}bg-studio-{timeline-bg, timeline-ruler-bg, timeline-playhead, timeline-snap, player-bg}text-studio-{text-primary, text-secondary, text-tertiary, text-disabled, inspector-label}border-studio-{border-subtle, border-default, border-strong}
And the standard Shadcn utilities — bg-card, bg-primary, text-foreground, bg-muted — also
work because the SDK maps them in the same @theme inline block.
Raw CSS variables
If you need full CSS power:
.my-floating-panel {
background: rgb(var(--studio-surface-float));
color: rgb(var(--studio-text-primary));
border: 1px solid rgb(var(--studio-border-default));
box-shadow: var(--studio-shadow-lg);
border-radius: var(--radius);
}The studio tokens are raw RGB triplets, so rgb(var(--studio-accent) / 0.6) and
rgb(var(--studio-accent) / 60%) both work.
Motion & layout tokens
The theme stylesheet defines a few non-color tokens you may want when matching the editor's feel:
| Token | Default | Use |
|---|---|---|
--studio-duration-fast | 100ms | Hover / quick state |
--studio-duration-medium | 200ms | Default transitions |
--studio-duration-slow | 300ms | Slide-in / slide-out |
--studio-ease-out | cubic-bezier(0.16, 1, 0.3, 1) | Reveal motion |
--studio-ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | Bidirectional motion |
--studio-z-base / -overlay / -modal / -toast | 1 / 10 / 50 / 100 | Layer ordering |
--studio-shadow-sm / -md / -lg | mode-aware | Card / popover / floating |
--studio-topbar-height | 48px | Toolbar height |
--studio-timeline-header-width | 192px | Width of the timeline track-name column |
These aren't in ThemeTokens (the TS type), but read them in CSS or set them via inline style if
you need to.
Where customization stops
Some things aren't customizable by token / icon / prop — they require either a plugin (extension points) or a full editor-shell fork:
| Want to change | Reach for |
|---|---|
| Add a sidebar tab | Plugin — registerPanel |
| Add an inspector section | Plugin — registerInspectorSection |
| Add a toolbar button | Plugin — registerToolbarAction |
| Change a hotkey | Plugin — registerHotkey (overrides default by id) |
| Add a context-menu item | Plugin — registerContextMenuAction |
| Replace the timeline renderer | Fork: use createDefaultStudio + your layout |
| Replace the player | Fork: render your own composition with StudioPlayer directly |
| Custom panel layout entirely | Fork: build with PanelLayout, InspectorPanel, TimelineRoot, StudioPlayer |
Most apps never need a fork — plugins and theme overrides cover real-world cases. If you do fork, the same UI primitives are exported individually so you can recompose the shell.
Recipes
Make the editor full-screen, with sticky header
<VideoStudioProvider
className="fixed inset-0" // covers the whole viewport
theme="dark"
>
<StudioEditor className="h-full" />
</VideoStudioProvider>Pin the brand color even on user-OS theme switch
<VideoStudioProvider
theme="system" // user's OS preference for surface scale
themeOverrides={{
primary: '124 58 237', // brand stays constant
studioAccent: '124 58 237',
}}
>Surface tokens follow OS theme; the accent stays branded.
Match the editor's accent in your own toolbar
<header className="flex items-center justify-between border-b border-studio-border-subtle bg-studio-surface-raised px-4 py-2">
<h1 className="text-studio-text-primary">My App</h1>
<button className="bg-studio-accent text-white hover:bg-studio-accent/90 rounded-md px-3 py-1.5">
Sign out
</button>
</header>Multi-tenant theming
function tenantTheme(tenantId: string): ThemeOverrides {
switch (tenantId) {
case 'tenant-a': return { primary: '14 165 233', studioAccent: '14 165 233' }
case 'tenant-b': return { primary: '236 72 153', studioAccent: '236 72 153' }
default: return {}
}
}
<VideoStudioProvider themeOverrides={tenantTheme(currentTenant)}>Or with per-mode tenancy:
const overrides: ThemeOverrides = {
light: tenantLightTokens[tenantId],
dark: tenantDarkTokens[tenantId],
}Brand the editor without forking the stylesheet
The single most concise rebrand:
<VideoStudioProvider
themeOverrides={{
primary: '14 165 233',
studioAccent: '14 165 233',
studioAccentHover: '2 132 199',
studioSelectionColor: '14 165 233',
}}
>That's 4 tokens for a recognisable brand identity. Add the surface + border tokens from the full rebrand recipe above for the rest.
Scope custom CSS to the editor only
If you write app-wide CSS that clashes with the editor, scope your overrides under .vsdk-root —
the SDK adds this class to its container:
.vsdk-root .my-extra-class {
/* applies only inside the editor */
}Conversely, if you need to escape the editor's tokens (rare), set all: revert on the boundary
of your component.
Debugging
| Symptom | Likely cause |
|---|---|
| Tokens applied but no visible change | Wrong format — RGB triplet, not rgb(...) or #hex |
| Accent on buttons changed but not on transform handles | You missed studioSelectionColor |
| Light mode looks fine, dark mode broken | Used a flat (non-per-mode) overrides object with values that only work in one mode |
Icons unchanged after icons={…} | Object identity changes every render — wrap in useMemo or move to module scope |
| Custom CSS isn't applying | The stylesheet import (@studio-dev/vsdk/styles.css) loads after yours — bump specificity or use @layer |