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 fit | Poor 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 UIs | Modals — render those in a Dialog triggered from elsewhere |
| "Insert at playhead" actions | One-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.