Custom Widget
Custom widgets let you build an OBS browser source with React, TypeScript and CSS. Use the Synchra API for chat messages, activities, giveaways, settings and stored values.
Quick setup
Section titled “Quick setup”- Open the Synchra Dashboard.
- Go to Widgets and create a Custom widget.
- Edit the project files, then choose Apply to update the preview.
- Use Test event below the preview to send sample chat messages or activities while editing.
- Click Widget URL and add it to OBS as a Browser Source.
Use Save to keep applied changes.
Chat example
Section titled “Chat example”Set font-size on a wrapper to change the logo size:
Activity example
Section titled “Activity example”See Activity data for activity fields and provider-independent filtering with activity and contribution groups.
Giveaway example
Section titled “Giveaway example”The hook shares its lifecycle with the built-in Giveaway widget: draw for four seconds, show the winner for 30 seconds, then refresh the newest open giveaway. No timers or event subscriptions are needed in custom code. The built-in widget also waits for its configured exit animation before refreshing.
Configure that behavior through options:
| Option | Default | Behavior |
|---|---|---|
giveawayId | null | Follow the newest open giveaway, even when its entry command is unavailable or its entry deadline has passed. An explicit ID stays pinned to that giveaway, including when it is closed. |
recentCount | 10 | Number of recent entries, limited to 0-10. |
drawDurationMs | 4000 | Time in the drawing phase. Set to 0 to reveal immediately. |
winnerDurationMs | 30000 | Time to show the revealed winner. |
exitDurationMs | 0 | Time in the exiting phase before removing the winner. Use it for a CSS exit animation. |
nextGiveawayDelayMs | Sum of the three durations | Delay measured from the draw event before refreshing selection. Set to 0 to fetch immediately in the background. Set longer than the presentation to add a blank interval afterward. |
An explicit ID refreshes that same giveaway after the delay. It never advances to another ID. Redraws restart the timing; changing the requested ID clears the previous presentation. Durations are nonnegative milliseconds; non-finite values use the defaults.
Without an explicit ID, live draw events from any giveaway on the channel show a winner, including closed giveaways that are no longer selected or were already closed when the widget loaded. With an explicit ID, only draws for that giveaway are shown. Duplicate delivery does not restart the presentation.
| Field | Meaning |
|---|---|
phase | idle, active, drawing, winner, exiting, or waiting. active means the giveaway is open and remains on screen, including while awaiting a winner after entries close. Only advertise entry when snapshot.is_running is true. waiting is the blank interval when the next-giveaway delay exceeds the presentation. |
winner | Current presentation, or null. Includes giveaway, finalized entryCount, drawing, and exiting. It remains stable if a background refresh selects another giveaway. |
snapshot | Server snapshot for the presented selection, or null when unavailable. snapshot.is_running indicates entry availability, not widget visibility; it accounts for command schedules and stream restrictions. trigger and end_at describe its linked entry command, if present. end_at closes entries, not the giveaway display. |
giveaway | Selected giveaway, reconciled from snapshots and live updates. During the next-giveaway delay it stays on the drawn giveaway. Explicit ID selection can return a closed giveaway and its saved winner without replaying an animation. |
entry_count, recent_entries | Live entries for the selected giveaway, held during the next-giveaway delay. A draw uses the server’s finalized count. Use these instead of the corresponding snapshot fields. |
last_event | Most recent observed giveaway event. It may refer to the previously selected giveaway, so check last_event.data.id when matching it to a selection. It persists until another event arrives. |
last_event.draw | Present only on a completed draw, including redraws of the same winner. draw.entry_count is the finalized count used for that draw. Ordinary edits and closing a giveaway do not include it. |
Both widget types keep displaying and listening to the selected open giveaway after its countdown ends, so a winner can be drawn later. Entry availability never causes selection to fall back to an older giveaway. Explicitly closing or deleting a giveaway removes it from latest selection; opening a newer giveaway selects that one. Refreshes from live events and reconnects are deferred during nextGiveawayDelayMs, which starts at the draw event, not the entry deadline. Remounting refreshes current state; missed draw events are not replayed. For custom presentation, last_event remains available, but most widgets only need phase and winner.
KV value example
Section titled “KV value example”Convert currencies
Section titled “Convert currencies”Fetch API data
Section titled “Fetch API data”Project files
Section titled “Project files”index.html is the project entry. It selects the widget module and styles:
Add external scripts or stylesheets to index.html with normal absolute URLs. Use relative imports for your own components and helpers.
Available packages:
react@tanstack/react-queryzustand
Custom font
Section titled “Custom font”See Custom fonts for loading and applying a custom font.
Live event callbacks
Section titled “Live event callbacks”Use callbacks when code outside a React component needs new events.
React API
Section titled “React API”Normal React hook rules apply: call hooks at the top level of a React component, not inside conditions or callbacks.
React hooks, useQuery, useMutation and Zustand’s create are available directly.
| API | Behavior |
|---|---|
synchra.render(node) | Renders the widget’s root React element. |
synchra.mode | preview in the editor and live in the browser source. |
synchra.settings | Values declared by the settings schema. |
synchra.useChatMessages({ limit?, types? }) | Recent messages. Defaults to message types. |
synchra.useLatestChatMessage({ types? }) | Latest message. Defaults to message types. |
synchra.useActivities({ limit?, types?, activityGroups?, contributionGroups? }) | Recent activities, optionally filtered by type or group. |
synchra.useLatestActivity({ types?, activityGroups?, contributionGroups? }) | Latest matching activity. |
synchra.useGiveaway(options?) | Shared giveaway lifecycle, configurable timing, winner and live entries. |
synchra.useKvValue(key) | Returns a channel KV value and its updates. |
synchra.kv.get({ key }) | Reads a channel KV key. |
synchra.kv.set({ key, value, ttl? }) | Writes a JSON value with an optional TTL in seconds. |
synchra.kv.delete({ key }) | Deletes a channel KV value. |
synchra.kv.inc({ key, amount?, ttl? }) | Atomically increments an integer value. |
synchra.convertCurrency({ amount, from, to }) | Converts an amount using the current currency rates. |
synchra.assembleParts(parts) | Renders message parts, including emotes and links. |
synchra.providerLogo(provider) | Renders a provider logo. Its size follows font-size. |
synchra.partsToText(parts) | Converts chat or activity message parts to readable text. |
User-editable settings
Section titled “User-editable settings”Open settings.schema.json in the Explorer to define the controls shown under Settings.
Read the values from synchra.settings.
Settings are also CSS custom properties. For example, accent_color becomes --accent-color.
New widgets include canvas_scale. It affects the browser source, not the editor preview, and can be removed from the schema.
Supported types: string, number, integer, and boolean.
title,description,default: labels and defaultsenum,enumItemLabels: select optionsminimum,maximum,multipleOf: number constraintsformat:color,textarea, orslider
Preview-only controls
Section titled “Preview-only controls”Use synchra.mode for controls that should appear in the editor preview but not in OBS.