Components
The UI primitives re-exported through @studio-dev/vsdk and @studio-dev/vsdk/ui — use them in your panels for visual consistency with the editor.
The SDK re-exports a curated set of UI primitives from @studio-dev/vsdk-ui (the internal component
library). They share the editor's theme tokens automatically, so anything you build with them slots
into the design without extra styling work.
What's available
| Group | Components |
|---|---|
| Buttons & inputs | Button, Input, Label, Checkbox, RadioGroup, Switch, Slider, Toggle, ToggleGroup |
| Layout | Separator, ScrollArea, ScrollBar, Tabs (with TabsList, TabsTrigger, TabsContent) |
| Overlays | Dialog, Sheet, Popover, Tooltip, DropdownMenu, ContextMenu |
| Selection | Select |
| Feedback | Progress, Skeleton, Badge, Kbd, KbdGroup |
| Custom | Editor-specific surfaces from ui-desktop/components (loading overlay, panels, etc.) |
The Button and Badge exports also include their CVA variants (buttonVariants, badgeVariants)
for composing custom triggers.
Importing
Two import paths give you the same set:
// Through the SDK barrel — recommended when you already import other SDK exports
import { Button, Input, Dialog, useStudioStore } from '@studio-dev/vsdk'
// Through the UI subpath — useful when you only need UI primitives
import { Button, Input, Dialog } from '@studio-dev/vsdk/ui'A third option pulls a single component from the underlying library — best for plugin authors who want minimal coupling to the SDK barrel:
import { Button } from '@studio-dev/vsdk-ui/components/button'
import { Dialog, DialogContent } from '@studio-dev/vsdk-ui/components/dialog'All three resolve to the same module. Pick one and be consistent within a plugin.
Why use these over your own?
- Theme awareness — they read the studio CSS variables, so your panel changes color when the user
switches light/dark or overrides
themeOverrides. - Behavior — most are Radix/Base UI underneath, so accessibility (keyboard focus, ARIA, focus traps for modals) is handled.
- Layout match — sizing and density match the editor's other surfaces — no "this panel feels bigger than everything else" problem.
Quick examples
Button (with CVA variants)
import { Button } from '@studio-dev/vsdk'
import { useStudioIcon } from '@studio-dev/vsdk'
function MyToolbar() {
const PlusIcon = useStudioIcon('add')
return (
<div className="flex gap-2">
<Button variant="default" size="sm">
<PlusIcon className="size-4" />
Add item
</Button>
<Button variant="secondary" size="sm">Secondary</Button>
<Button variant="ghost" size="sm">Ghost</Button>
<Button variant="destructive" size="sm">Delete</Button>
</div>
)
}Variants: default, secondary, ghost, destructive, outline, link. Sizes: sm, default,
lg, icon.
Input / Slider
import { Input, Label, Slider } from '@studio-dev/vsdk'
function OpacityControl({ value, onChange }: { value: number; onChange: (n: number) => void }) {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="opacity">Opacity</Label>
<Slider
id="opacity"
min={0}
max={1}
step={0.01}
value={[value]}
onValueChange={([v]) => onChange(v)}
/>
<Input
type="number"
min={0}
max={1}
step={0.01}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
</div>
)
}Popover
import { Popover, PopoverTrigger, PopoverContent, Button } from '@studio-dev/vsdk'
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm">Options</Button>
</PopoverTrigger>
<PopoverContent className="w-56">
{/* …menu items… */}
</PopoverContent>
</Popover>Tabs (good fit for plugin sub-views)
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@studio-dev/vsdk'
<Tabs defaultValue="presets">
<TabsList>
<TabsTrigger value="presets">Presets</TabsTrigger>
<TabsTrigger value="custom">Custom</TabsTrigger>
</TabsList>
<TabsContent value="presets">…</TabsContent>
<TabsContent value="custom">…</TabsContent>
</Tabs>Tooltip
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@studio-dev/vsdk'
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button>?</button>
</TooltipTrigger>
<TooltipContent>Helpful explanation</TooltipContent>
</Tooltip>
</TooltipProvider>Keyboard shortcut hint
import { Kbd, KbdGroup } from '@studio-dev/vsdk'
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>Z</Kbd>
</KbdGroup>Utility exports
A handful of helpers come along the same paths:
import {
cn, // class concatenator (clsx + tailwind-merge)
generateId, generateItemId, // nanoid-based, prefixed
framesToSeconds, secondsToFrames, framesToTimecode, framesToDisplayTime,
timecodeToFrames, msToFrames, framesToMs, clampFrame,
} from '@studio-dev/vsdk'| Helper | Purpose |
|---|---|
cn(...classes) | Merge class lists; preserves Tailwind precedence |
generateId(prefix?) | Random id like cmd_abc123 |
generateItemId / generateTrackId / generateMarkerId / generateRegionId / generateProjectId / generateAssetId / generateJobId | Prefixed ids for the corresponding entity |
framesToSeconds(frames, fps) | Frame → seconds |
secondsToFrames(seconds, fps) | Seconds → frame |
framesToTimecode(frames, fps) | "hh:mm:ss:ff" |
framesToDisplayTime(frames, fps) | "m:ss" (UI-friendly) |
timecodeToFrames("hh:mm:ss:ff", fps) | Inverse of framesToTimecode |
msToFrames(ms, fps) / framesToMs(frames, fps) | Millisecond bridge |
clampFrame(frame, min, max) | Clamp a frame number |
Anchor for timeline-aware UI
If your panel needs to know where the user is on the timeline (e.g. "Insert at playhead"), prefer the SDK helpers over reimplementing logic:
import { findAvailableTrack, useStudioStore } from '@studio-dev/vsdk'
const tracks = useStudioStore((s) => s.timeline.tracks)
const items = useStudioStore((s) => s.timeline.items)
const itemIdsByTrack = useStudioStore((s) => s.timeline.itemIdsByTrack)
const currentFrame = useStudioStore((s) => s.player.currentFrame)
const target = findAvailableTrack(tracks, itemIdsByTrack, items, 'track', currentFrame, 90)That returns the first track of the given kind with room at the playhead — exactly what the filters/typography panels use when inserting.
Icon system
Override icons globally, look icons up at runtime, register plugin icons. Complete reference of every icon name.
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.