Hotkey
Keyboard shortcut registration — scopes, modifiers, and how they interact with built-ins.
A hotkey registers a keyboard shortcut. The SDK calls your onTrigger when the user presses the
key combination — scoped to a specific focus zone or "always" (works anywhere in the editor).
Registration shape
interface HotkeyRegistration {
id: string
key: string // 'e', 'd', 'ArrowLeft' — see notes below
modifiers?: ('ctrl' | 'shift' | 'alt' | 'meta')[]
label: string // shown in shortcut listings
onTrigger: () => void
when?: 'always' | 'timeline-focused' | 'player-focused'
}Registering one
ctx.registerHotkey({
id: 'mycompany:apply-effect',
key: 'e',
modifiers: ['ctrl'],
label: 'Apply effect',
when: 'always',
onTrigger: () => {
const state = ctx.store.getState()
// Apply effect to selected items, for example
for (const id of state.selection.selectedItemIds) {
state.updateItemProperties(id, { /* … */ })
}
},
})Field reference
id
Unique. The registry de-duplicates by id.
key
The KeyboardEvent code or key (the SDK matches on lowercased input). Single letters work as
expected ('e', 's'). For named keys use the standard names: 'ArrowLeft', 'Escape', 'Space',
'Enter'.
modifiers
Combination of 'ctrl', 'shift', 'alt', 'meta'. On macOS, meta is Cmd. The SDK uses
ctrl || meta interchangeably for cross-platform shortcuts — pass ['ctrl'] and it triggers on
both Cmd+key (Mac) and Ctrl+key (Windows).
label
Human-readable description. Shown in any shortcut-listing UI the host app builds (the SDK doesn't ship one by default).
when
Scope — when the hotkey is active:
| Value | Fires when |
|---|---|
'always' (default) | The user is anywhere in the editor and not typing in an input |
'timeline-focused' | The timeline panel has focus |
'player-focused' | The player has focus |
All three scopes ignore inputs, textareas, selects, and contenteditable — the user never accidentally triggers a hotkey while typing.
onTrigger
The handler. Same conventions as toolbar actions: read state via ctx.store.getState(), dispatch
via store actions, batch multi-step changes.
Avoid collisions with built-ins
The SDK reserves a set of hotkeys via useHotkeys():
| Shortcut | Built-in action |
|---|---|
Space | Play / pause toggle |
S | Split selected item at playhead |
Delete / Backspace | Remove selected items |
⌘D / Ctrl+D | Duplicate selected items |
⌘Z / Ctrl+Z | Undo |
⌘⇧Z / Ctrl+Shift+Z | Redo |
⌘A / Ctrl+A | Select all |
Escape | Deselect all |
← / → | Nudge selection by 1 frame (or seek if no selection) |
Shift+← / Shift+→ | Nudge / seek by 10 frames |
+ / - | Zoom in / out |
Home / End | Jump to start / end |
Don't register hotkeys that collide with these — the user will get unpredictable behavior. Pick
something free (Shift+letter, Alt+letter, or a less-loaded modifier combo).
Patterns
Repeat-friendly action
Hotkeys repeat by default while the key is held. For actions you don't want repeating (e.g. "add marker"), guard with a flag:
let armed = true
ctx.registerHotkey({
id: 'mycompany:add-marker',
key: 'm',
label: 'Add marker',
when: 'always',
onTrigger: () => {
if (!armed) return
armed = false
const state = ctx.store.getState()
state.addMarker(state.player.currentFrame)
requestAnimationFrame(() => { armed = true })
},
})A cleaner approach is to listen for keyup yourself — but that requires bypassing the registration
helper, which most plugins don't need.
Shortcut that mirrors a toolbar action
If you register the same logic as both a toolbar action and a hotkey, factor the implementation out:
function applyEffect(store: StudioStore) { /* … */ }
ctx.registerToolbarAction({
id: 'mycompany:apply-effect',
label: 'Apply effect',
icon: ctx.icons.sliders,
order: 50,
onClick: () => applyEffect(ctx.store),
})
ctx.registerHotkey({
id: 'mycompany:apply-effect-shortcut',
key: 'e',
modifiers: ['ctrl'],
label: 'Apply effect',
when: 'always',
onTrigger: () => applyEffect(ctx.store),
})
// You can also use it as the displayed shortcut on the context-menu entry:
ctx.registerContextMenuAction({
id: 'mycompany:apply-effect-menu',
label: 'Apply effect',
shortcut: '⌘E',
order: 50,
onAction: () => applyEffect(ctx.store),
})The user can discover the action through any of the three surfaces; the implementation lives in one place.