@fcannizzaro/streamdeck-react

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 --watch

Install 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

  1. defineAction() maps a UUID to a React component and provides info for manifest generation.
  2. googleFont() downloads the font from Google Fonts (cached to disk after the first run).
  3. createPlugin() prepares fonts, registers actions, and creates the shared runtime.
  4. plugin.connect() attaches to the Stream Deck SDK.
  5. During the build, the bundler plugin extracts info from each defineAction() call and generates manifest.json automatically.
  6. When the action appears, @fcannizzaro/streamdeck-react mounts a React root for that hardware instance (or resumes a recycled one).
  7. State changes trigger re-renders, and the renderer pushes a new image to the device.

Next Steps

On this page