Video Studio SDKv0.0.3
Plugin authoringExtension points

Panel

Sidebar panel registration — desktop and mobile components, ordering, icons, and lazy loading.

A panel is a piece of UI mounted in the sidebar. On desktop it lives in the left-hand icon sidebar; on mobile it's an entry in the bottom sheet. Most plugins ship one panel as their primary surface (e.g. "Filters", "Stock", "GIF", "Stickers").

The registration shape

interface PanelRegistration {
  id:              string
  label:           string
  icon:            ComponentType<{ className?: string }>
  component:       LazyExoticComponent<any> | ComponentType<any>
  mobileComponent?: LazyExoticComponent<any> | ComponentType<any>
  order:           number
  mobileIcon?:     ComponentType<{ className?: string }>
}

Registering one

import { lazy } from 'react'
import { createPlugin } from '@studio-dev/vsdk/plugins'

export const myPlugin = () =>
  createPlugin({
    id: 'mycompany:my-plugin',
    name: 'My Plugin',
    version: '0.1.0',
    onRegister(ctx) {
      ctx.registerPanel({
        id:        'my-panel',
        label:     'My Panel',
        icon:      ctx.icons.pluginElements,
        component: lazy(() => import('./panel')),
        order:     50,
      })
    },
  })

id — uniqueness

The registry de-duplicates by id. Re-registering with the same id is a no-op. Namespace it (vendor:purpose) to avoid clashes with built-ins (filters, stock, gif, typography).

icon — pull from the resolved map

icon: ctx.icons.pluginElements,

ctx.icons is the resolved icon map at the time of registration (defaults + consumer overrides). Using it means a host-app icon override of pluginElements also changes your panel's sidebar icon — desirable for cohesion.

If you want a panel-specific icon outside the catalog, register it first and reach for it the same way:

ctx.registerIcons({ myPanelIcon: MyPanelIconComponent })
ctx.registerPanel({
  // ...
  icon: ctx.icons.myPanelIcon,
})

component — almost always React.lazy

Use React.lazy so the panel ships only when the user actually opens it. The SDK wraps the component in Suspense automatically.

component: lazy(() => import('./panel'))

For tiny static panels you can pass a plain component:

component: function MyPanel() { return null }

Either works — lazy is the default-good choice.

order — slot in the sidebar

Built-in plugins use 1 to 10. Use 50, 60, 70 for plugin-tier slots. The registry sorts ascending — a panel with order: 50 appears before one with order: 60.

mobileComponent — only if you need it

If your panel needs a different layout on mobile (touch-friendly buttons, bottom-sheet ergonomics), provide a second component:

component:       lazy(() => import('./panel')),
mobileComponent: lazy(() => import('./panel-mobile')),

When mobileComponent is omitted, the SDK uses component on both desktop and mobile.

mobileIcon — only if the desktop one doesn't fit

The desktop icon is used for mobile too unless mobileIcon is provided. Most plugins don't need this.

Panel component contract

The component receives no props. Read state through hooks; write through actions.

'use client'

import { Button, useStudioStore } from '@studio-dev/vsdk'

export default function MyPanel() {
  const itemCount = useStudioStore((s) => Object.keys(s.timeline.items).length)
  const undo = useStudioStore((s) => s.undo)

  return (
    <div className="flex flex-col gap-2 p-3">
      <p>Items: {itemCount}</p>
      <Button onClick={() => undo()}>Undo</Button>
    </div>
  )
}

What to put in a panel

Good fitPoor fit
Asset browsers (stock, GIFs, brand kits)Anything mutating a specific item — use an inspector
Preset libraries (filters, animations, typography)Global app navigation — use sidebarButtons
Search UIsModals — render those in a Dialog triggered from elsewhere
"Insert at playhead" actionsOne-off buttons — register as a toolbar action

Layout conventions

The sidebar gives your panel a fixed width (~280px) and full height. Build with flex columns and a scroll area for long lists:

<div className="flex h-full flex-col">
  <div className="border-b p-3">{/* header */}</div>
  <div className="flex-1 overflow-y-auto p-3">{/* scrollable content */}</div>
  <div className="border-t p-3">{/* footer */}</div>
</div>

The studio surface tokens give a consistent feel — bg-studio-surface-raised for cards, border-studio-border-subtle for dividers, text-studio-text-secondary for hints. See UI → Color system.

Rendering plugin panels in your own UI

If you're building a custom editor shell, render panels by iterating usePanels():

import { usePanels } from '@studio-dev/vsdk'
import { Suspense, useState } from 'react'

function CustomSidebar() {
  const panels = usePanels()
  const [active, setActive] = useState(panels[0]?.id ?? null)
  const ActivePanel = panels.find((p) => p.id === active)?.component
  return (
    <aside>
      <nav>{/* render p.icon for each */}</nav>
      <Suspense fallback={null}>{ActivePanel && <ActivePanel />}</Suspense>
    </aside>
  )
}

usePanels() returns the array sorted by order. The StudioEditor shell does exactly this — your custom shell can follow the same pattern.

Opening a specific panel by default

The provider's defaultActivePanel prop accepts a panel id:

<VideoStudioProvider defaultActivePanel="my-panel">

Use this when you want your plugin's panel to be the user's first surface.

On this page