Video Studio SDKv0.0.3
Guides

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:

LayerWhat you changeSurface
1. Provider propsOne-line knobs — theme, logo, defaultActivePanel, sidebarButtons, iconsThe cosmetic top-layer of the editor
2. Theme token overridesColor tokens — accent, surfaces, borders, text, track colorsEvery Tailwind utility / CSS-var-driven surface
3. Plugin-contributed UIAdd panels / inspector sections / toolbar buttonsNew, custom surfaces
4. Tailwind utilities + custom CSSAny pixel-level stylingCustom 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.

<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:

TokenDefaultUse
--studio-duration-fast100msHover / quick state
--studio-duration-medium200msDefault transitions
--studio-duration-slow300msSlide-in / slide-out
--studio-ease-outcubic-bezier(0.16, 1, 0.3, 1)Reveal motion
--studio-ease-in-outcubic-bezier(0.4, 0, 0.2, 1)Bidirectional motion
--studio-z-base / -overlay / -modal / -toast1 / 10 / 50 / 100Layer ordering
--studio-shadow-sm / -md / -lgmode-awareCard / popover / floating
--studio-topbar-height48pxToolbar height
--studio-timeline-header-width192pxWidth 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 changeReach for
Add a sidebar tabPlugin — registerPanel
Add an inspector sectionPlugin — registerInspectorSection
Add a toolbar buttonPlugin — registerToolbarAction
Change a hotkeyPlugin — registerHotkey (overrides default by id)
Add a context-menu itemPlugin — registerContextMenuAction
Replace the timeline rendererFork: use createDefaultStudio + your layout
Replace the playerFork: render your own composition with StudioPlayer directly
Custom panel layout entirelyFork: 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

SymptomLikely cause
Tokens applied but no visible changeWrong format — RGB triplet, not rgb(...) or #hex
Accent on buttons changed but not on transform handlesYou missed studioSelectionColor
Light mode looks fine, dark mode brokenUsed 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 applyingThe stylesheet import (@studio-dev/vsdk/styles.css) loads after yours — bump specificity or use @layer

Where to go next

On this page