Troupeby AliveDocsSign in

Integrate

The React components

@alivelabs/troupe-react is the set of themable React components that Troupe Studio is built from, for showing and driving devices in your own page. This page covers DeviceScreen, the other components and theming.

Install

Shell
bun add --exact @alivelabs/troupe-react @alivelabs/troupe-client

Like the rest of Troupe, the packages are not on npm yet; Install has the status.

  • react and react-dom 19.2 or newer are peer dependencies.
  • The components run in a browser with WebSocket and WebCodecs. Server rendering is not supported.
  • The package ships TypeScript sources, so your bundler must compile TypeScript inside node_modules: Vite, Bun.build and esbuild do.
  • Import the stylesheet once. Your app needs no Tailwind or other CSS framework.

A live device

DeviceScreen shows a session's live stream and lets the viewer drive it.

TSX
import { createClient } from '@alivelabs/troupe-client'
import { DeviceScreen } from '@alivelabs/troupe-react'
import '@alivelabs/troupe-react/styles.css'

const troupe = createClient({ baseUrl, token })
const stream = await troupe.call('streams.ticket', { sessionId, cap: 'drive' })

<DeviceScreen
  stream={stream}
  getTicket={(cap) => troupe.call('streams.ticket', { sessionId, cap: cap ?? 'drive' })}
/>

stream is the first { url, ticket }. getTicket mints another when the stream needs one: after a ticket is refused, and when the viewer presses Take control.

The component handles the rest:

  • It decodes the video onto a canvas and reconnects when the socket drops.
  • It sends pointer, wheel and keyboard input while the viewer drives. Shift+Escape leaves the device, since Tab and Escape go to it.
  • Its toolbar shows the controls the device offers now: Back, Home and the app switcher, Rotate, Lock or Power, Volume, and a Screenshot button that saves the picture as a PNG.
  • Its status line says who drives and why input is unavailable when it is.
  • It releases the decoder while the page is hidden.
PropDefaultWhat it does
streamRequired{ url, ticket } from streams.ticket, or undefined while there is none
getTicketnoneReturns a fresh { url, ticket } for the capability it is given
chrometruetrue shows the toolbar and the status line, 'compact' one toolbar row, false the picture only
layout'intrinsic''intrinsic' keeps the device's aspect ratio; 'fill' takes the parent's size
maxHeightnoneA cap on the picture's height
bezelfalseDraws a dark phone frame around the picture
screenshottruefalse hides the Screenshot button
fps, bitratenoneThe frame rate and the bits per second to ask for, where the device takes live settings
adaptivefalseSteps the frame rate and bitrate down and up with the connection
highlightnoneA frame to outline on the picture, such as the inspector's hovered element
toolbarnoneExtra toolbar content
platformnoneThe session's platform id. It picks the buttons' faces
onStatusnoneCalled with the connection's status on every change
onInputnoneCalled once the viewer's input has been quiet for a moment
onRotatenoneCalled on Rotate instead of sending the stream's rotate message
label, classNameThe view's accessible name, and a class for its root

The other components

Each of these takes a client prop, or reads the client from a TroupeProvider above it.

ComponentWhat it doesMain props
TroupeProviderMakes a client available to the components below itclient
SessionListLists sessions and keeps the list current from sessions.events. End releases oneselectedId, onSelect, onReleased, filter ('live' or 'all')
CreateSessionFormA form for sessions.create: the platform, then its device types and OS versionsonCreated, onCancel, holderLabel
A11yTreeThe accessibility inspector: the tree in aria form, a filter, the selected element's properties and a suggested locatorsessionId, onHighlight, onSelect, refreshKey
LogsFollows logs.stream, with a text filter and a lowest levelsessionId, capacity
ScreenshotButtonTakes a media.screenshot and downloads it, or hands it to onCapturesessionId, format, onCapture
ErrorNoticeShows any error as text, with the fix of an environment errorerror, onRetry

Together they make an inspector:

TSX
import { useState } from 'react'
import type { TroupeClient } from '@alivelabs/troupe-client'
import type { StreamSource } from '@alivelabs/troupe-client/stream'
import type { SessionId, StreamCap } from '@alivelabs/troupe-protocol'
import {
  A11yTree,
  type DeviceHighlight,
  DeviceScreen,
  Logs,
  TroupeProvider,
} from '@alivelabs/troupe-react'

interface InspectorProps {
  troupe: TroupeClient
  sessionId: SessionId
  stream: StreamSource
  getTicket: (cap?: StreamCap) => Promise<StreamSource>
}

function Inspector({ troupe, sessionId, stream, getTicket }: InspectorProps) {
  const [highlight, setHighlight] = useState<DeviceHighlight | null>(null)
  const [driven, setDriven] = useState(0)

  return (
    <TroupeProvider client={troupe}>
      <DeviceScreen
        stream={stream}
        getTicket={getTicket}
        highlight={highlight}
        onInput={() => setDriven((count) => count + 1)}
      />
      <A11yTree sessionId={sessionId} onHighlight={setHighlight} refreshKey={driven} />
      <Logs sessionId={sessionId} />
    </TroupeProvider>
  )
}

Hovering a line in the tree outlines that element on the device. Driving the device changes refreshKey, so the tree takes a new snapshot.

For a view of your own, the package also exports the logic behind the components: useTroupeClient, useResource for loading a value with its state, ariaRows and suggestLocator for the tree, appendLogs and filterLogs for logs, applySessionEvent and buildSpec for sessions, and describeError.

Text that comes from a device or an app, such as an accessibility label or a log line, is always rendered as text, never as HTML.

Theming

styles.css is one plain stylesheet. Its classes start with troupe-, and it has no global reset, so it does not change the rest of your page.

Every colour, border and focus ring reads one of 24 custom properties. Each has a light and a dark default through CSS light-dark(), so the components follow your page's color-scheme with no work. To theme them, set the properties on a container:

.device-panel {
  color-scheme: dark;
  --troupe-bg: #0b1020;
  --troupe-surface: #141a2e;
  --troupe-border-focus: #9d7bff;
  --troupe-accent: #2a2350;
}
GroupProperties
Surfaces--troupe-bg, --troupe-surface, --troupe-accent
Borders--troupe-border, --troupe-border-strong, --troupe-border-focus
Text--troupe-fg, --troupe-fg-muted, --troupe-fg-dim
States--troupe-ok, --troupe-pill-idle-bg, --troupe-pill-idle-fg
Errors--troupe-danger-surface, --troupe-danger-line, --troupe-danger-fg
Notices--troupe-notice-surface, --troupe-notice-border, --troupe-notice-fg
Logs--troupe-log-stdout, --troupe-log-stdout-tag, --troupe-log-stderr, --troupe-log-stderr-tag, --troupe-log-system, --troupe-log-system-tag

The package exports the list as THEME_VARIABLES. The bezel is not themed: a phone's frame is dark on any page.

Next

esc

    Type to search every page, the protocol reference and the cloud API.