Color system
Theme tokens, light/dark modes, per-instance overrides, Tailwind utilities, and the programmatic API.
The SDK's color system is a layered token model on top of CSS custom properties. Every surface in the editor reads from a named token; you override the token, every dependent surface updates.
Three layers
- Studio primitives — a 12-step warm-neutral gray scale (
--studio-gray-100…--studio-gray-950) plus a violet accent set. Raw RGB channel triplets (e.g."124 58 237") so they compose with alpha utilities. - Studio semantic tokens — derived from the primitives. Surfaces (
--studio-surface-ground,…-raised,…-overlay,…-float), borders (-subtle/-default/-strong), text (-primary/-secondary/-tertiary/-disabled), timeline (--studio-timeline-*), track type colors (--studio-track-video,…-audio, etc.). - Shadcn-style semantic tokens —
--background,--foreground,--primary,--card, etc. These are computed from the studio tokens and consumed by the UI primitives (Button, Dialog, etc.).
The flow is primitives → studio semantic → shadcn semantic → components. Override at any level and everything downstream updates.
Light vs dark
The .vsdk-root element holds the light-theme tokens. Adding .dark switches them to the dark
palette. Both are defined in @studio-dev/vsdk/styles.css. The provider toggles the class for you
based on the theme prop and prefers-color-scheme.
<VideoStudioProvider theme="light"> {/* always light */}
<VideoStudioProvider theme="dark"> {/* always dark */}
<VideoStudioProvider theme="system"> {/* default — follow OS */}You can read the resolved theme via the data-theme attribute on the root: data-theme="light" or
data-theme="dark".
Overriding tokens
From the provider
themeOverrides accepts either a flat map (applies to both modes) or a per-mode object.
// Flat — same value in light and dark
<VideoStudioProvider
themeOverrides={{
primary: '124 58 237',
studioAccent: '124 58 237',
studioAccentHover: '109 40 217',
}}
>
// Per-mode
<VideoStudioProvider
themeOverrides={{
light: { primary: '30 80 50', studioAppBg: '245 245 245' },
dark: { primary: '155 120 255', studioAppBg: '20 18 28' },
}}
>Values are raw RGB channel strings ("R G B" with spaces, no rgb() wrapper). This is what lets
Tailwind utilities like bg-studio-accent/30 apply alpha correctly.
Programmatically (anywhere)
import { applyThemeTokensToElement, createThemeTokens } from '@studio-dev/vsdk'
const tokens = createThemeTokens({
primary: '124 58 237',
studioAccent: '124 58 237',
})
const root = document.querySelector('.vsdk-root') as HTMLElement
applyThemeTokensToElement(root, tokens)applyThemeTokensToElement writes the values as inline style properties on the element, which take
precedence over the stylesheet. Any unknown keys are emitted under the --vsdk- prefix.
Per-mode programmatic resolution
resolveThemeOverrides returns the correct token bag for the current mode:
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' }The provider does this internally — exposed here for advanced use (e.g. live theme editors).
Full token map
Shadcn semantic (RGB channels unless noted)
| Token | Purpose |
|---|---|
background | Page background |
foreground | Default text color |
primary | Brand color (buttons, links, ring) |
primaryForeground | Text on primary surfaces |
secondary | Secondary surface |
secondaryForeground | Text on secondary |
muted | Muted surface (input bg, tag bg) |
mutedForeground | Muted text |
accent | Accent surface (hover states) |
accentForeground | Text on accent |
border | Default border |
input | Input field background |
ring | Focus ring |
radius | Root radius (CSS length, e.g. "0.5rem") |
Studio gray palette (RGB channels)
studioGray100 (lightest in dark mode / darkest in light mode) through studioGray950. Twelve steps.
The semantic surface/border/text tokens are derived from these.
Studio surface hierarchy
| Token | Default mapping |
|---|---|
studioAppBg | --studio-gray-950 — outermost app background |
studioSurfaceGround | --studio-gray-900 — main editor surface |
studioSurfaceRaised | --studio-gray-850 — panels, cards |
studioSurfaceOverlay | --studio-gray-800 — popovers, dropdowns |
studioSurfaceFloat | --studio-gray-750 — floating layers (toasts, drag previews) |
Studio borders / text
| Token | Use |
|---|---|
studioBorderSubtle | Default divider |
studioBorderDefault | Default border |
studioBorderStrong | Heavy border / outline |
studioTextPrimary | Main text |
studioTextSecondary | Lighter body text |
studioTextTertiary | Hint, helper |
studioTextDisabled | Disabled state |
Studio accent
| Token | Purpose |
|---|---|
studioAccent | The editor's accent (violet by default) |
studioAccentHover | Hover state |
studioAccentSubtle | Soft accent surface |
studioSelectionColor | Transform-handle and selection outline color |
Track type colors
Each timeline track type has its own color:
| Token | Track |
|---|---|
studioTrackVideo | Video items |
studioTrackAudio | Audio items |
studioTrackText | Text items |
studioTrackImage | Image items |
studioTrackOverlay | Overlay items |
studioTrackShape | Shape items |
studioTrackCaptions | Captions |
studioTrackEffect | Effects |
studioTrackAdjustment | Adjustment layers |
Timeline
| Token | Use |
|---|---|
studioTimelineBg | Timeline panel background |
studioTimelineRulerBg | Ruler strip |
studioTimelineRulerText | Ruler labels |
studioTimelineTrackBorder | Border between tracks |
studioTimelinePlayhead | Playhead line (red by default) |
studioTimelineSnap | Snap guide line (amber by default) |
Inspector / player
| Token | Use |
|---|---|
studioInspectorInputBg | Input background in the inspector |
studioInspectorLabel | Label color in the inspector |
studioPlayerBg | Player letterbox background |
Using tokens from your own code
As Tailwind utilities
The SDK registers a Tailwind v4 @theme inline block that exposes the studio tokens as utilities. You
can use them directly in className strings:
<div className="bg-studio-surface-raised text-studio-text-primary border border-studio-border-subtle rounded-lg">
...
</div>
<span className="text-studio-track-audio">Audio clip</span>
<div className="bg-studio-accent/30">Soft accent surface</div>Available utility prefixes:
bg-studio-{app-bg,surface-ground,surface-raised,surface-overlay,surface-float},
bg-studio-{accent,track-*,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}.
As raw CSS variables
.my-panel {
background: rgb(var(--studio-surface-raised));
color: rgb(var(--studio-text-primary));
border: 1px solid rgb(var(--studio-border-subtle) / 0.6);
}Because the values are raw RGB channel triplets, you can compose alpha with rgb(... / 0.6) syntax.
As shadcn semantic utilities
Standard shadcn utility names work because the SDK maps them in the same @theme inline block:
<div className="bg-card text-card-foreground border-border">…</div>
<button className="bg-primary text-primary-foreground hover:bg-primary/90">Save</button>Motion & layout tokens
The theme stylesheet also defines a few non-color tokens you may want when matching the editor's feel:
| Token | Default |
|---|---|
--studio-duration-fast | 100ms |
--studio-duration-medium | 200ms |
--studio-duration-slow | 300ms |
--studio-ease-out | cubic-bezier(0.16, 1, 0.3, 1) |
--studio-ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) |
--studio-z-base / -overlay / -modal / -toast | 1 / 10 / 50 / 100 |
--studio-shadow-sm / -md / -lg | mode-aware soft shadows |
--studio-topbar-height | 48px |
--studio-timeline-header-width | 192px |
These aren't part of ThemeTokens (the type), but you can read them in CSS or set them via inline
style.
Recipes
Match the editor's accent in your own UI
<button className="bg-studio-accent text-white hover:bg-studio-accent/90 rounded-md px-3 py-1.5">
My button
</button>Build a small "filter pill" using surface + accent tokens
<button className="bg-studio-surface-raised text-studio-text-secondary border border-studio-border-subtle px-2 py-1 rounded data-[active=true]:bg-studio-accent-subtle data-[active=true]:text-studio-text-primary">
Filter
</button>Brand the editor without forking the stylesheet
<VideoStudioProvider
themeOverrides={{
primary: '14 165 233', // sky-500
studioAccent: '14 165 233',
studioAccentHover: '2 132 199',
studioSelectionColor: '14 165 233',
}}
>Two lines and the editor's accent, focus ring, primary button, and selection color all shift together.