Skip to main content

Property schema

A property is a control a streamer configures when they add your widget — a colour, a number, a font, a toggle. You define your widget's properties on the Properties surface, and Community generates the configuration form from them automatically. There's nothing to wire up: define a property, and it appears in the panel beside the widget.

Each property you read in two places:

  • In code as props.<name> (or the useProp('<name>') hook in a template).
  • In CSS as a variable, var(--<name>).

Defining a property​

On the Properties surface, click Add property and fill in:

FieldMeaning
KeyThe stable identifier (e.g. accent-colour). Everything else derives from it. Don't rename it after release — it's how saved configurations are matched.
TypeThe kind of control (see below).
LabelWhat the streamer sees above the control.
DescriptionOptional helper text under the control.
Default valueThe value used until the streamer changes it.
GroupOptional section heading to organise a long form. A group can also be placed on a tab of a settings modal.
RequiredMark the field as required.

Field types​

TypeControlNotable options
textText boxmultiline, placeholder, maxLength
numberNumber inputmin, max, step, suffix (e.g. px), slider to render as a slider
rangeSlidermin, max, step (required), suffix
booleanSwitchdisplay: 'checkbox' for a checkbox instead
selectDropdownselectOptions (the choices); display: 'segmented' for inline buttons
colorColour pickeralpha for transparency; swatches for suggested colours
swatchPreset colour pickerswatches — the locked set the streamer picks from
extended-colorRich fill editorA solid colour, gradient or image fill. Normally an object — read it off props, not CSS — but it arrives as a bare CSS string (#f00, linear-gradient(...)) when the streamer binds the field to live data, so handle both.
imageAsset pickerPicks an image from the streamer's library.
audioAsset pickerPicks a sound from the streamer's library.
fontFont pickerPicks one of the platform fonts.
directionAngle dialDegrees 0–360.
channel-point-redeemChannel-point reward pickerThe streamer picks one of their own Twitch rewards; the value is the reward's Twitch id.
power-upPower-up pickerThe streamer picks one of their Twitch Power-Ups; the value is its id.
twitch-categoryTwitch category searchThe value is a { id, name } object.
textStyleText style pickerPicks a text style, with an Edit style shortcut. See Text styles.
group-listOrdered listA drag-reorderable list of entries, each with its own fields. The Workshop's type picker calls it Ordered list. See Ordered lists.
tip

The channel-point-redeem, power-up and twitch-category pickers pair naturally with bindings. Add a channel-point-redeem property and match its value against the rewardId on a Channel Points event to fire only for that reward.

Text styles (textStyle)​

A textStyle property lets the streamer pick a text style. Your widget doesn't receive the style's name. It receives the finished look, ready to hand to StyledText.

  • The default can only be a built-in style, or none. A saved style belongs to one streamer, and a widget installed from the Workshop must never point into the author's library. The Properties surface only offers built-in styles for the default, and saving a widget with a saved style as the default is refused.
  • The streamer picks their own on each widget they place, from their saved and built-in styles.
  • It can't be made dynamic. Like other pickers, it has no template toggle.
  • Edits are live. When the streamer saves their style, your widget gets the new look without a reload.
  • On a public widget page, visitors can only pick built-in styles.
  • To let a flow choose the style, use a Text style parameter on the event instead.

Reading a property in code​

A property's key becomes a camelCase name on the props object. So accent-colour → props.accentColour:

import { onLoad, props } from '@widget';

onLoad(() => {
console.log(props.accentColour); // the configured value, or the default
});

Inside a template, use the useProp hook so the template re-renders when the streamer changes the value:

import { useProp } from '@widget';

export default function Main() {
const colour = useProp('accentColour');
return <div style={{ color: colour }}>Hello</div>;
}

Reading a property in CSS​

The same key becomes a kebab-case CSS variable. So accent-colour → var(--accent-colour):

.title {
color: var(--accent-colour);
}

This is the easiest way to wire a colour or size straight into your styles — no code needed.

Naming

The two names are derived from the key automatically (accent-colour → props.accentColour and var(--accent-colour)). If you need a different name, set an explicit JS name or CSS variable on the property to override the derived one.

Show a field only when it's relevant​

A property can declare a visibility condition so its control only appears when another property has a certain value. Write a small boolean expression that references sibling properties by their JS name:

showLogo === true && logoOpacity > 50

The logo-opacity control then only shows when Show logo is on and opacity is above 50. If the expression can't be evaluated, the field is shown anyway — a bad condition never hides a control by mistake. Hidden fields still resolve to their value in code and CSS; this only affects the form.

Ordered lists (group-list)​

A group-list property is an ordered list the streamer builds and reorders by dragging. Use it for anything with a variable number of entries: a set of zones, a list of messages, a stack of layers. In the Workshop's type picker it's called Ordered list. Like any property, it can sit in a group on a tab of a settings modal.

Each entry has an entry type, and each entry type has its own fields. Set these under options:

OptionMeaning
itemKindsThe entry types. Each is id, label, optional description, schema (the entry's fields) and optional max (how many of this type).
maxItemsThe most entries the list can hold.
itemsToggleableGive each entry an on/off switch.
addLabelText for the add button.
itemNounA singular noun for the entries, such as zone.
dynamicSubFieldsShow the dynamic toggle on each entry's eligible fields, so entry values can be macro templates.

With several entry types, the streamer picks from an Add menu. With one, they get a plain add button.

A field in schema is a normal property field, with one exception: it can't be a group-list itself. Lists go one level deep.

defaultValue must be an array, usually [].

Reading the list in code​

useProp('myList') returns the entries in order. Each is { uuid, kind, values }:

  • kind is the entry type's id.
  • values is keyed by each field's JS name, so the key stay-seconds becomes staySeconds.
  • Disabled entries are left out.

Key your React list on uuid, not the index. The index changes when the streamer reorders.

import { useProp } from '@widget';

export default function Main() {
const messages = useProp('messages');
return (
<ul>
{messages.map((m) => (
<li key={m.uuid}>{m.values.text}</li>
))}
</ul>
);
}

Limits​

  • A list can't be made dynamic as a whole. Only its entries' fields can, with dynamicSubFields.
  • A list publishes no CSS variable.
  • Don't rename an entry type id or a field key after you publish. Streamers' saved entries are discarded.

Canvas regions​

Mark an entry type as a region and each of its entries becomes a box, a single point, or a path of points that the streamer places on the widget in the Overlay Editor. Use boxes for exclusion zones avatars avoid, areas they gather in, platforms for a physics widget, or keep-off areas so effects avoid the facecam. Use points for spawn points and anchors. Use paths for routes that walkers follow, and closed paths (polygons) for areas that aren't rectangles, such as an irregular floor.

Add region to the entry type:

OptionMeaning
shape'rect' (the default) for a box, 'point' for a single point, 'path' for a line of points, or 'circle' for a round area.
closedPaths only. The last point joins the first, making a polygon.
colorThe region's colour on the canvas.
labelFieldThe key of a text field on this entry type. Its value is shown on the region, such as an attractor's !gather. Without it, the entry type's label is shown.
defaultSizeBoxes and circles. A new entry's w and h as fractions. Defaults to 25% by 25% for a box and a 20% diameter for a circle, whose w is its diameter and h is ignored. New entries appear centred.

Each entry of a box type also carries a rect, of type RegionRect: { x, y, w, h }. These are fractions from 0 to 1 of the widget's own box, top left plus size, always kept fully inside the widget. Each entry of a point type carries a point, of type RegionPoint: { x, y }, the same kind of fractions. Each entry of a path type carries points, an array of 2 to 32 RegionPoints in order (a closed path needs at least 3). Each entry of a circle type carries a circle, of type RegionCircle: { x, y, r }. The centre x is a fraction of the widget's width and y a fraction of its height, and the radius r is a fraction of the widget's width, so the circle is always round: in pixels its centre is (x * width, y * height) and its radius is r * width. Multiply by useSize() to get pixels. Resizing the widget keeps regions in proportion.

Helpers from @widget do the geometry most region widgets need:

  • regionToPixels(item, size) gives an entry's rect, point or points in pixels. Pass size from useSize().
  • pointInRegion(item, x, y, size?) tells you whether a spot is inside a box or a polygon. Pass size and give x/y in pixels.
  • randomPointInRegion(item) picks a random spot inside a box or polygon, for spawning or wandering.
  • pointAlongPath(item.points, t, { closed, size }) gives the spot a fraction t of the way along a path, with angle, the direction of travel. On a closed path t wraps, so elapsed / period loops forever. Passing size keeps the pace and angle right on a wide widget.
// A marker walking each route, once around every 8 seconds
import { pointAlongPath, useNow, useProp, useSize } from '@widget';

export default function Walkers() {
const routes = useProp('routes');
const size = useSize();
const now = useNow(50); // a smooth tick; the default is once a second
return (
<>
{routes.map((route) => {
const at = pointAlongPath(route.points ?? [], now / 8000, { closed: true, size });
return (
<div
key={route.uuid}
style={{
position: 'absolute',
left: at.x * size.width,
top: at.y * size.height,
transform: `rotate(${at.angle}rad)`,
}}
>
🚶
</div>
);
})}
</>
);
}
// Property definition
{
key: 'zones',
type: 'group-list',
label: 'Zones',
defaultValue: [],
options: {
itemNoun: 'zone',
itemKinds: [
{
id: 'exclusion',
label: 'Exclusion zone',
schema: [],
region: { color: '#e03131' },
},
{
id: 'attractor',
label: 'Attractor',
region: { color: '#f5c518', labelField: 'command' },
schema: [
{ key: 'command', type: 'text', label: 'Command', defaultValue: '!gather' },
],
},
],
},
}
import { regionToPixels, useProp, useSize } from '@widget';

export default function Main() {
const zones = useProp('zones');
const size = useSize();
return (
<>
{zones.map((z) => {
const { rect } = regionToPixels(z, size);
return rect && (
<div
key={z.uuid}
style={{ position: 'absolute', left: rect.x, top: rect.y, width: rect.w, height: rect.h }}
/>
);
})}
</>
);
}

In the Overlay Editor​

The Overlay Editor draws each region as a box in its colour, with its label, the same way whether the widget is selected or not. Don't draw your own region markers in the widget's editor view, or they appear twice. A selected widget has an Edit zones button above it. The button is named after the list's itemNoun, or says Edit regions without one. Press it, or double-click the widget, to edit the regions.

While editing:

  • The bar above the widget has an add button for each region type. A new entry appears centred and selected.
  • A selected path shows a square handle on each point, and a hollow handle in the middle of each segment. Drag a hollow handle to add a point there. Drag the line, or a polygon's inside, to move the whole path.
  • With a path's point selected, arrow keys and Delete act on that point. A path keeps at least 2 points (3 when closed), so deleting at that minimum removes the whole path.
  • Drag a box to move it. Drag the handles to resize.
  • Arrow keys nudge the selected region. Hold Shift to move 10 px.
  • Delete removes the selected region.
  • Esc clears the selection, then leaves. Done leaves too.
  • Regions snap to the widget's edges and centre, to each other, and to the edges and centres of other widgets that overlap it, such as a facecam frame. Hold Ctrl or Shift to place freely.

The properties panel lists every entry. Hovering a row highlights its box, and clicking one selects it, even a box hidden under another. Below the list is the selected entry: its fields, its on/off switch when itemsToggleable is set, X, Y, Width and Height in percent, and Duplicate and Delete.

Region lists are edited only this way. They don't appear with the widget's other properties or in a settings modal, so every entry type in a region list must be a region. A list that mixes region and ordinary types gets the group-region-mixed-kinds warning.

Regions respect variants like any property. While a variant is previewed, edits go to that variant.

In the Workshop​

On a group-list property, each entry type has a Draw on canvas checkbox. Turn it on, then set:

  • Shape: box, point or path.
  • Closed: paths only. Joins the last point to the first.
  • Outline colour
  • Label: one of that type's text fields, or the type's label.
  • New entry size, in percent. Boxes only.

To place test regions, press Edit zones in the live preview's top-left corner and draw them on the preview, exactly as in the Overlay Editor. They're stored as Simulate test values, which the Simulate panel also lists with X, Y, Width and Height inputs (X and Y for points; a path's points are only edited on the preview). Editing turns on keeping those values, so the preview uses them on every surface.

Next​