vdo-streamdeck

Stream Deck Plugin Best Practices

Reusable checklist and rationale for building Elgato Stream Deck plugins. Written from the 2026-07-05 review of the VDO.Ninja plugin; each practice notes why it matters and, where relevant, where this repo applies it. Sizes and format rules below come from the official manifest schema (@elgato/schemas/streamdeck/plugins/manifest.json), not from memory.

1. Icons and image assets

Every image slot has its own spec — don’t reuse one tile everywhere

Slot Manifest field Size (@1x / @2x) Format Style rule
Plugin icon (preferences/marketplace) Icon (root) 256×256 / 512×512 PNG only Full-color allowed
Category icon (action list header) CategoryIcon 28×28 / 56×56 PNG or SVG Monochrome #FFFFFF on transparent
Action list icon Actions[].Icon 20×20 / 40×40 PNG or SVG Monochrome #FFFFFF on transparent
Key state image States[].Image 72×72 / 144×144 PNG, SVG, or GIF Full-color; this is what shows on the key
Multi-action image States[].MultiActionImage 72×72 / 144×144 PNG or SVG Full-color
Encoder (dial) icon Encoder.Icon 72×72 / 144×144 PNG or SVG Shown in circular dial canvas
Touchscreen background Encoder.background 200×100 / 400×200 PNG or SVG Behind the touch layout

Why it matters: a dark full-color key tile used as an action-list icon renders as a muddy square on the dark action-list background and fails marketplace review. The list wants small white glyphs; the key wants a rich tile. They are different assets with different jobs.

Practices:

2. Manifest checklist

3. Property inspector UX and onboarding

Setup-first, then get out of the way

The property inspector is the only onboarding surface a Stream Deck plugin has. Two competing needs:

  1. A first-time user who drops any action must be led through setup without reading docs.
  2. A configured user who clicks an action wants that action’s settings immediately — not two screens of setup to scroll past.

Resolution used here and recommended generally:

Status language: report the state that matters to the user

“Connected to relay” and “the app you want to control answered” are different states, and users only care about the second. Enumerate explicit states with plain-language messages — e.g. missing key / connecting / waiting for page / connected / timeout / error — and phrase the waiting state as an instruction (“Open your generated link and keep that page open”), not a protocol report.

Use color + text together (a red dot alone is not accessible); keep <label for=...> on every field.

Reduce manual URL/config editing

If setup involves the user constructing a URL or config string, build it for them: page-type dropdown, the two or three fields that matter, generated output with Copy / Open / QR buttons. Generate credentials (API keys) with one click using crypto.getRandomValues. Every manual editing step is a support ticket.

Group buttons by what they operate on: key actions (Generate / Copy / Show / Test) next to the key field, URL actions (Copy / Open / QR) next to the generated URL. A misplaced button (Show Key living in the URL row) reads as belonging to the wrong object.

Settings persistence pitfalls

Native look

Elgato’s sdpi-components web components give the property inspector the native Stream Deck look (correct fonts, spacing, dark theme) for free and handle the settings plumbing. Hand-rolled CSS is acceptable (this plugin’s is close), but budget for matching future Stream Deck theme changes yourself.

4. Runtime behavior

Never let the plugin process die

The Stream Deck runtime does not resurrect your plugin gracefully mid-session; an uncaught exception kills every key. Real example from this plugin’s logs: ws.close() on a socket still in CONNECTING state throws, and a global-settings change mid-connect crashed the whole process. Rules:

State feedback discipline

Dangerous actions

Two-press confirmation (arm on first press, execute within a window) is a good pattern for destructive commands (hangup, transfer-all). Requirements: visible armed feedback, a short expiry (~2s), automatic visual reset on expiry, and a per-action settings toggle so power users can disable it.

Continuous controls (dials/encoders)

Momentary keys (push-to-talk)

Send explicit on/off for key-down/key-up rather than toggle twice, and use per-key sequence counters so a stale async completion can’t repaint the key after a newer release event.

5. Testing and release workflow

Runnable without hardware, in order:

npm test                                   # pure-logic units: payload builders, parsers, state normalization
npm run check                              # tsc --noEmit
npm run build                              # emit + copy into .sdPlugin
npx @elgato/cli validate <sdPlugin> --no-update-check
npx @elgato/cli pack <sdPlugin> --dry-run --no-update-check

6. Marketplace/distribution checklist

7. Things done well in this plugin worth repeating elsewhere