@fcannizzaro/streamdeck-react

TouchStrip Component

Render a single shared React component across the full Stream Deck+ touch strip using the touchStrip option.

The touchStrip option in defineAction replaces per-encoder dial rendering with a single shared React tree that spans the entire Stream Deck+ touch strip (800x100 pixels for 4 encoders).

This is useful for animations, games, visualizations, or any UI that needs to render across the full strip width rather than in isolated 200x100 segments.

Defining a TouchStrip Action

Pass a touchStrip component to defineAction instead of dial:

import { defineAction, useTouchStrip, useTick } from "@fcannizzaro/streamdeck-react";

function MyTouchStrip() {
  const { width, height, fps } = useTouchStrip();

  return (
    <div style={{ width, height, background: "#1a1a2e" }}>
      <span style={{ color: "white", fontSize: 16 }}>
        Full strip: {width}x{height}
      </span>
    </div>
  );
}

export const myAction = defineAction({
  uuid: "com.example.plugin.touchstrip",
  touchStrip: MyTouchStrip,
  touchStripFPS: 30,
});

Place this action on all encoder slots in the Stream Deck app. They will share a single render.

How It Works

AspectPer-encoder dialShared touchStrip
React rootsOne per encoder slotOne for the full strip
Canvas size200x100 per slotUp to 800x100 (4 slots)
EventsPer-instance, no column neededAll events include column to identify the dial
Render pipelineEach root renders + pushes independentlyOne render, then sliced into 200px segments and pushed per-encoder

When touchStrip is set, the library:

  1. Creates a single TouchStripRoot per device.
  2. Renders your component at the full strip width.
  3. Slices the rendered image into 200px segments.
  4. Pushes each slice to its corresponding encoder via setFeedback.

touchStripFPS

Controls both the useTick cadence and the render debounce interval.

export const myAction = defineAction({
  uuid: "com.example.plugin.action",
  touchStrip: MyTouchStrip,
  touchStripFPS: 30, // 30fps render loop
});

The FPS value is available inside the component via useTouchStrip().fps, which you should pass to useTick so the animation loop matches the render pipeline:

function MyTouchStrip() {
  const { fps } = useTouchStrip();

  useTick((delta) => {
    // delta is in milliseconds
    // runs at the configured touchStripFPS rate
  }, fps);

  // ...
}

Default is 30fps (the maximum supported by Stream Deck hardware).

Geometry

The touch strip geometry is available via useTouchStrip():

  • width: Full strip width. Computed as (maxColumn + 1) * 200. With 4 encoders at columns 0-3, width is 800.
  • height: Always 100 pixels.
  • segmentWidth: Always 200 pixels per encoder.
  • columns: Sorted array of active encoder columns (e.g. [0, 1, 3]). Adapts dynamically as encoders appear/disappear.

Handling All 4 Dials

Since all encoder events arrive in one component, use the column field to distinguish which dial fired:

import {
  useTouchStripDialRotate,
  useTouchStripDialDown,
  useTouchStripTap,
} from "@fcannizzaro/streamdeck-react";

function MyTouchStrip() {
  useTouchStripDialRotate(({ column, ticks }) => {
    switch (column) {
      case 0:
        /* dial 0 logic */ break;
      case 1:
        /* dial 1 logic */ break;
      case 2:
        /* dial 2 logic */ break;
      case 3:
        /* dial 3 logic */ break;
    }
  });

  useTouchStripDialDown(({ column }) => {
    if (column === 2) pause();
    if (column === 3) restart();
  });

  useTouchStripTap(({ tapPos, column }) => {
    // tapPos is absolute across the full strip
    // column identifies which segment was tapped
  });

  // ...
}

Manifest Configuration

The manifest action must use "Controllers": ["Encoder"] and provide a layout with a canvas pixmap:

{
  "Actions": [
    {
      "UUID": "com.example.plugin.touchstrip",
      "Name": "My TouchStrip",
      "Controllers": ["Encoder"],
      "Encoder": {
        "layout": "layouts/touchstrip.json",
        "TriggerDescription": {
          "Rotate": "Action description",
          "Push": "Action description",
          "Touch": "Action description"
        }
      }
    }
  ]
}

The layout file (layouts/touchstrip.json):

{
  "id": "com.streamdeck-react.touchstrip-layout",
  "items": [
    {
      "key": "canvas",
      "type": "pixmap",
      "rect": [0, 0, 200, 100]
    }
  ]
}

Complete Example

A scrolling animation with dial controls:

import { useState, useRef } from "react";
import {
  defineAction,
  useTouchStrip,
  useTouchStripDialRotate,
  useTouchStripDialDown,
  useTick,
} from "@fcannizzaro/streamdeck-react";

function ScrollBar() {
  const { width, height, fps } = useTouchStrip();
  const [offset, setOffset] = useState(0);
  const [speed, setSpeed] = useState(1);
  const [paused, setPaused] = useState(false);

  useTick((delta) => {
    if (paused) return;
    setOffset((o) => (o + delta * 0.05 * speed) % width);
  }, fps);

  useTouchStripDialRotate(({ column, ticks }) => {
    if (column === 0) {
      setSpeed((s) => Math.max(0.5, Math.min(5, s + ticks * 0.5)));
    }
  });

  useTouchStripDialDown(({ column }) => {
    if (column === 2) setPaused((p) => !p);
  });

  return (
    <div style={{ width, height, position: "relative", overflow: "hidden", background: "#0d1117" }}>
      <div
        style={{
          position: "absolute",
          left: offset,
          top: 30,
          width: 40,
          height: 40,
          borderRadius: 20,
          background: "#58a6ff",
        }}
      />
    </div>
  );
}

export const scrollAction = defineAction({
  uuid: "com.example.plugin.scroll",
  touchStrip: ScrollBar,
  touchStripFPS: 30,
});

See Also

On this page