Quick Start
Build your first Stream Deck plugin with @fcannizzaro/streamdeck-react.
This guide creates a minimal counter action, bundles it with Vite 8+ (Rolldown), and connects it to the Stream Deck runtime.
If you want the scaffold generated for you, start with create-streamdeck-react. The steps below show the manual setup.
1. Create a Plugin Project
Start with a normal Stream Deck plugin folder structure. You don't need to write a manifest.json by hand — it's auto-generated at build time from your defineAction({ info }) calls and the bundler plugin's manifest option.
2. Install Dependencies
bun add @fcannizzaro/streamdeck-react react
bun add -d [email protected] @vitejs/[email protected]Native Takumi bindings are lazy-loaded by default — they are downloaded from npm on first plugin startup and cached on disk. No platform-specific packages need to be installed.
3. Create an Action Component
// src/actions/counter.tsx
import { useState } from "react";
import { defineAction, useKeyDown, useKeyUp, cn } from "@fcannizzaro/streamdeck-react";
function CounterKey() {
const [count, setCount] = useState(0);
const [pressed, setPressed] = useState(false);
useKeyDown(() => {
setCount((value) => value + 1);
setPressed(true);
});
useKeyUp(() => {
setPressed(false);
});
return (
<div
className={cn(
"flex h-full w-full flex-col items-center justify-center gap-1",
pressed
? "bg-linear-to-br from-[#2563eb] to-[#1d4ed8]"
: "bg-linear-to-br from-[#0f172a] to-[#1e293b]",
)}
>
<span className="text-[12px] font-semibold uppercase tracking-[0.2em] text-white/70">
Count
</span>
<span className="text-[34px] font-black text-white">{count}</span>
</div>
);
}
export const counterAction = defineAction({
uuid: "com.example.react-basic.counter",
key: CounterKey,
info: {
name: "Counter",
icon: "imgs/actions/counter",
},
});4. Create the Plugin Entry
// src/plugin.ts
import { createPlugin, googleFont } from "@fcannizzaro/streamdeck-react";
import { counterAction } from "./actions/counter.tsx";
const inter = await googleFont("Inter");
const plugin = createPlugin({
fonts: [inter],
actions: [counterAction],
});
await plugin.connect();5. Add a Bundler Config
// vite.config.ts
import { builtinModules } from "node:module";
import { resolve } from "node:path";
import { defineConfig, esmExternalRequirePlugin } from "vite";
import react from "@vitejs/plugin-react";
import { streamDeckReact } from "@fcannizzaro/streamdeck-react/vite";
const PLUGIN_DIR = "com.example.react-basic.sdPlugin";
const builtins = builtinModules.flatMap((m) => [m, `node:${m}`]);
export default defineConfig({
resolve: {
conditions: ["node"],
},
plugins: [
esmExternalRequirePlugin({ external: builtins }),
react(),
streamDeckReact({
uuid: "com.example.react-counter",
manifest: {
uuid: "com.example.react-counter",
name: "React Counter",
author: "Your Name",
description: "A simple counter plugin.",
icon: "imgs/plugin-icon",
version: "0.0.0.1",
},
}),
],
build: {
target: "node20",
outDir: resolve(PLUGIN_DIR, "bin"),
emptyOutDir: false,
sourcemap: true,
minify: false,
lib: {
entry: resolve("src/plugin.ts"),
formats: ["es"],
fileName: () => "plugin.mjs",
},
rolldownOptions: {
output: {
codeSplitting: false,
},
},
},
});Native bindings are lazy-loaded by default — they are downloaded from npm on first plugin startup. To copy binaries from node_modules instead, set nativeBindings: "copy" and pass explicit targets.
6. Dev
npx vite build --watchInstall the generated .sdPlugin in the Stream Deck app, place the action on a key, and press it to see the counter update.
If your package.json has a dev script configured, you can also just run bun dev (or npm run dev / pnpm dev).
What's Happening
defineAction()maps a UUID to a React component and providesinfofor manifest generation.googleFont()downloads the font from Google Fonts (cached to disk after the first run).createPlugin()prepares fonts, registers actions, and creates the shared runtime.plugin.connect()attaches to the Stream Deck SDK.- During the build, the bundler plugin extracts
infofrom eachdefineAction()call and generatesmanifest.jsonautomatically. - When the action appears,
@fcannizzaro/streamdeck-reactmounts a React root for that hardware instance (or resumes a recycled one). - State changes trigger re-renders, and the renderer pushes a new image to the device.