Skip to content

StreamSync Overlays — Reference

🍼 ELI5

An overlay is the on-screen stuff viewers see over your gameplay — alert pop-ups, a chat box, a "now playing" label. Drag widgets onto a canvas in the Visual Builder, no code required, then point an OBS Browser Source at the overlay's link and it updates live as things happen on stream.

Overlays are HTML/CSS/JS pages served by a local HTTP server and loaded as Browser Sources in OBS Studio (or any other software that accepts a URL). StreamSync injects a WebSocket client into every overlay page so the page receives live stream events with no extra setup.


How it works

When StreamSync starts, it launches an HTTP + WebSocket server on port 3838. The port is fixed — if 3838 is already in use by another app, StreamSync will log an error rather than switching ports, so your OBS Browser Source URLs never change unexpectedly.

  • http://localhost:3838/overlays/<id> — serves the overlay HTML page
  • ws://localhost:3838/ws — WebSocket endpoint; auto-injected into every overlay page
  • http://localhost:3838/ — lists all saved overlays

The WebSocket client is injected into every overlay's <head> automatically. It:

  • Connects to ws://localhost:3838/ws on page load
  • Auto-reconnects every 3 seconds if the connection drops
  • Dispatches incoming events as CustomEvents on window, so your JS uses window.addEventListener('chat.message', ...) — no WebSocket boilerplate needed

Visual Builder

The Visual Builder is the default authoring mode for new overlays. It requires no HTML, CSS, or JavaScript knowledge — you pick widgets, position them on a 1920 × 1080 canvas, and configure their appearance through a properties panel. Generated code is produced automatically on save.

Widget types

WidgetWhat it does
TextStatic label. Set font, size, color, weight, alignment, and optional drop shadow.
Chat BoxLive chat feed. Configurable direction (bottom-up or top-down), max message count, font size, and per-user name colors.
AlertEvent pop-up for follows, subs, raids, cheers, and redeems. Template text supports {name} interpolation. Has animation (fade, slide-down, slide-up, bounce) and auto-dismiss timer.
Latest EventPersists the last follower/subscriber/raider/cheerer name on screen. Uses {name} template. Updates in real-time as events arrive.
ImageURL-sourced or local-file image. Supports contain/cover/stretch fit and opacity. Local files are copied to the assets folder and served over HTTP so OBS can load them.
Stream InfoDisplays live stream title, game/category, or viewer count. Updates when StreamSync sends a stream.info event.

Builder vs code mode

New overlays always open in builder mode. Existing overlays that were hand-coded open in code mode, unchanged.

To switch a builder overlay to raw code: click Edit code… in the action bar and confirm the prompt. This is one-way — the generated code becomes editable but the builder data is discarded.

The Generated code collapsible (bottom of the editor) lets you inspect or copy the HTML/CSS/JS output of a builder overlay without leaving builder mode.

Testing widgets

A Test: row of buttons sits below the builder canvas. Clicking any button immediately broadcasts a sample event to all connected overlay pages (OBS Browser Sources or Preview windows). Use these to trigger alert animations and verify chat box behaviour without real viewer activity.

ButtonEvent fired
Followfollow — displayName: TestUser
Subscribesubscribe — tier 1000
Raid (42)raid — 42 viewers
Cheer 100cheer — 100 bits
Redeemredeem — Test Reward
Chatchat.message — "Hello! PogChamp"

Note: Test events go to connected overlay pages, not the builder canvas preview. Open the overlay via Open or load it in OBS first.


Creating and editing overlays

Builder mode (default for new overlays)

  1. Open the app → click Overlays in the nav
  2. Click + New
  3. Type a name — the URL ID is auto-generated (e.g. "My Chat" → my-chat)
  4. Click widgets in the Blank or Library palette to add them to the canvas
  5. Drag elements to reposition; select one to edit its properties on the right
  6. Click Save — HTML/CSS/JS is generated automatically
  7. Click Open to preview, or Copy to get the OBS URL

Code mode

To write raw HTML/CSS/JS instead:

  1. Click + New, give the overlay a name
  2. Click Edit code… in the action bar (if in builder mode, confirm the prompt)
  3. Edit the HTML, CSS, and JS tabs directly
  4. Click Save

To edit an existing overlay, click its name in the sidebar.


Overlay files

Each overlay is stored as its own .json file inside the overlays/ folder:

apps/streamsync/overlays/
├── .gitignore                  ← root *.json files are gitignored (personal)
├── example-overlay/
│   └── example-overlay.json   ← bundled starter overlay (tracked in git)
└── widgets/
    └── ...                    ← widget library (see below)

In development, overlays you create land as flat files in overlays/ (e.g. overlays/my-chat.json). These are gitignored by default so personal overlays don't appear in the repo.

In a packaged build, overlays are stored in the app's userData directory (%APPDATA%\streamsync\overlays\ on Windows).

Sharing an overlay

An overlay .json looks like this:

json
{
  "id": "my-chat",
  "name": "My Chat",
  "builderElements": [ ... ],
  "html": "...(generated)...",
  "css":  "...(generated)...",
  "js":   "...(generated)...",
  "createdAt": 1750000000000,
  "updatedAt": 1750000000000
}

To share an overlay:

  1. Find the file in overlays/ (or export it from the sidebar — coming soon)
  2. Send the .json file to someone
  3. They drop it into their own overlays/ folder and it appears in the sidebar on next launch

Overlays placed in a sub-folder act as read-only bundled presets — deleting them from the app removes only the root copy, and the preset reappears automatically. This is how the example-overlay/ folder works.


Widget Library

The widget library gives the Visual Builder a palette of pre-configured, shareable widgets. Widgets are .json files in overlays/widgets/, grouped into sub-folders by category.

overlays/widgets/
├── alerts/
│   ├── follow-alert.json
│   ├── subscribe-alert.json
│   ├── raid-alert.json
│   └── cheer-alert.json
├── chat/
│   └── bottom-up-chat.json
├── stream-info/
│   ├── viewer-count.json
│   └── game-title.json
└── latest-events/
    ├── latest-follower.json
    └── latest-subscriber.json

Dropping any .json into this tree (in the right sub-folder, or a new one) makes it appear in the Library section of the builder palette on next launch. No code changes required.

Built-in widgets

Alerts

FileDescription
follow-alert.jsonPurple slide-down — new follower
subscribe-alert.jsonGold bounce-in — new subscriber
raid-alert.jsonRed slide-up — incoming raid
cheer-alert.jsonTeal fade-in — bit cheer

Chat

FileDescription
bottom-up-chat.jsonTransparent feed, newest messages at the bottom

Stream Info

FileDescription
viewer-count.jsonCurrent viewer count (updates on stream.info)
game-title.jsonCurrent game/category

Latest Events

FileDescription
latest-follower.jsonPersists most recent follower name
latest-subscriber.jsonPersists most recent subscriber name

Creating a custom widget

A widget file is a single .json that describes one pre-configured element. When a user clicks it in the palette, StreamSync assigns it a fresh ID and adds it to the canvas — so one widget file can be used many times without conflicts.

Minimal template

json
{
  "id": "my-widget",
  "name": "My Widget",
  "description": "A short description shown in the palette",
  "category": "My Category",
  "element": {
    "id": "_tpl",
    "type": "alert",
    "x": 25, "y": 35, "w": 50, "h": 16,
    ...element properties...
  }
}

Save the file anywhere inside overlays/widgets/ (sub-folders become the default category if you omit the category field). Restart the app and the widget appears in the Library palette.

The id inside element ("_tpl" here) is always replaced with a fresh random ID when the widget is dropped onto the canvas. Any placeholder value works.

Field reference

FieldRequiredDescription
idYesUnique slug for the widget file (e.g. "follow-alert-red")
nameYesDisplay name in the palette
descriptionNoOne-line description shown below the name
categoryNoPalette group heading. Defaults to the sub-folder name (capitalised).
elementYesThe full element definition (see element types below)

Element types and their properties

All element types share these base position fields (values are percentages of the 1920 × 1080 canvas):

FieldTypeDescription
xnumberLeft edge (0–100)
ynumberTop edge (0–100)
wnumberWidth (0–100)
hnumberHeight (0–100)
rotationnumber (optional)Tilt in degrees, -180–180. Only Text, Image, and Video widgets expose a control for it in the builder — see below.

Rotating an element

Select a Text, Image, or Video widget on the canvas and a small handle appears above it, connected by a thin line (below it instead, if the widget sits near the top edge of the canvas). Drag the handle to tilt the element around its center; hold Shift while dragging to snap to 15° steps. The properties panel also has a numeric Rotation ° field for setting an exact angle. Rotating an element that also has the Alert Add-on enabled keeps the tilt through the entrance animation — it doesn't reset when the alert plays.


"type": "text" — Static label

json
{
  "id": "_tpl",
  "type": "text",
  "x": 5, "y": 5, "w": 30, "h": 6,
  "content": "Now Live!",
  "fontSize": 48,
  "fontFamily": "sans-serif",
  "fontWeight": "bold",
  "color": "#ffffff",
  "textAlign": "left",
  "textShadow": true
}
FieldTypeValues
contentstringText to display
fontSizenumberpx
fontFamilystringCSS font family (e.g. "sans-serif", "Georgia")
fontWeightstring"normal" | "bold"
colorstringCSS color
textAlignstring"left" | "center" | "right"
textShadowbooleanAdds a subtle drop shadow

"type": "chatbox" — Live chat feed

json
{
  "id": "_tpl",
  "type": "chatbox",
  "x": 2, "y": 40, "w": 28, "h": 55,
  "maxMessages": 12,
  "fontSize": 22,
  "useNameColors": true,
  "bgColor": "transparent",
  "textColor": "#ffffff",
  "direction": "bottom-up"
}
FieldTypeValues
maxMessagesnumberMessages kept visible before oldest is trimmed
fontSizenumberpx
useNameColorsbooleanUse each viewer's Twitch name colour
bgColorstringCSS color ("transparent" = no background)
textColorstringCSS color
directionstring"bottom-up" (newest at bottom) | "top-down" (newest at top)

"type": "alert" — Event pop-up

json
{
  "id": "_tpl",
  "type": "alert",
  "x": 25, "y": 35, "w": 50, "h": 16,
  "event": "follow",
  "template": "New Follower! {name}",
  "duration": 5,
  "animation": "slide-down",
  "bgColor": "rgba(145,70,255,0.9)",
  "textColor": "#ffffff",
  "fontSize": 42,
  "borderRadius": 12
}
FieldTypeValues
eventstring"follow" | "subscribe" | "raid" | "cheer" | "redeem" | "subscribe.gift" | "watch_streak.milestone"
templatestringDisplay text. {name} is replaced with the relevant username.
durationnumberSeconds the alert stays visible
animationstring"fade" | "slide-down" | "slide-up" | "bounce"
bgColorstringCSS color (supports rgba for transparency)
textColorstringCSS color
fontSizenumberpx
borderRadiusnumberpx
minTotal / maxTotalnumber (optional)Only used when event is "subscribe.gift" — only fires when the gift batch's total falls in this range. Leave a bound unset to skip it.
minStreak / maxStreaknumber (optional)Only used when event is "watch_streak.milestone" — only fires when the viewer's streak count falls in this range. Leave a bound unset to skip it.

{name} resolves to:

EventValue
followdisplayName
subscribedisplayName
raidfromUserName
cheerdisplayName
redeemdisplayName
subscribe.giftgifterName
watch_streak.milestonedisplayName

"type": "latest-event" — Latest event display

json
{
  "id": "_tpl",
  "type": "latest-event",
  "x": 2, "y": 92, "w": 32, "h": 6,
  "event": "follow",
  "label": "Latest Follower:",
  "template": "{name}",
  "labelFontSize": 20,
  "valueFontSize": 24,
  "labelColor": "#aaaaaa",
  "valueColor": "#9146ff"
}
FieldTypeValues
eventstring"follow" | "subscribe" | "raid" | "cheer"
labelstringPrefix label text
templatestringValue text. {name} is replaced with the username.
labelFontSizenumberpx
valueFontSizenumberpx
labelColorstringCSS color
valueColorstringCSS color

json
{
  "id": "_tpl",
  "type": "image",
  "x": 80, "y": 3, "w": 16, "h": 16,
  "src": "https://example.com/logo.png",
  "opacity": 100,
  "objectFit": "contain"
}
FieldTypeValues
srcstringFull URL (https://…) or a local asset URL (http://localhost:3838/assets/…)
opacitynumber0–100
objectFitstring"contain" | "cover" | "fill"

To use a local file: open the widget's properties in the builder and click Browse local file…. StreamSync copies the file to the assets folder and fills in the URL automatically.


"type": "streaminfo" — Stream metadata

json
{
  "id": "_tpl",
  "type": "streaminfo",
  "x": 2, "y": 2, "w": 20, "h": 8,
  "field": "viewers",
  "label": "Viewers:",
  "labelFontSize": 24,
  "valueFontSize": 28,
  "labelColor": "#aaaaaa",
  "valueColor": "#ffffff"
}
FieldTypeValues
fieldstring"title" | "game" | "viewers"
labelstringPrefix label text
labelFontSizenumberpx
valueFontSizenumberpx
labelColorstringCSS color
valueColorstringCSS color

Stream info widgets update when StreamSync broadcasts a stream.info event. This happens automatically from the Dashboard. You can also trigger it manually:

js
window.streamSync.overlay.sendEvent({
  type: 'stream.info',
  data: { title: 'My Stream', game: 'Minecraft', viewers: 42 }
});

Full custom widget example

A red "Cheer" alert for cheers over 500 bits:

json
{
  "id": "big-cheer-alert",
  "name": "Big Cheer Alert",
  "description": "Red slide-up for cheers (use with a 500-bit threshold in your bot)",
  "category": "Alerts",
  "element": {
    "id": "_tpl",
    "type": "alert",
    "x": 20, "y": 30, "w": 60, "h": 20,
    "event": "cheer",
    "template": "{name} dropped a massive cheer!",
    "duration": 7,
    "animation": "slide-up",
    "bgColor": "rgba(200,30,30,0.95)",
    "textColor": "#ffff00",
    "fontSize": 44,
    "borderRadius": 8
  }
}

Save as overlays/widgets/alerts/big-cheer-alert.json, restart the app, and it appears under Library → Alerts in the builder palette.


OBS Browser Source setup

  1. In OBS: Sources → + → Browser
  2. Set URL to the overlay's URL (e.g. http://localhost:3838/overlays/chat-box)
  3. Set width/height to 1920 × 1080 (or match your canvas size)
  4. Enable "Shutdown source when not visible" and "Refresh browser when scene becomes active"
  5. For transparent background: add to your overlay CSS:
    css
    body { background: transparent; }
    The overlay server already injects body { background: transparent; } by default, so this is handled automatically.

Note: The overlay server only listens on 127.0.0.1 (localhost). It is not accessible from other machines on your network.


Receiving events in overlay JS

Every event type from the bot/EventSub system arrives as a CustomEvent on window. The event type matches the StreamSync event type string.

js
window.addEventListener('chat.message', function(e) {
  const data = e.detail.data;
  console.log(data.displayName, data.message);
});

e.detail is the full event object:

js
{
  platform:  'twitch',
  type:      'chat.message',
  timestamp: 1719500000000,  // Unix ms
  data:      { /* event-specific payload — see below */ }
}

platform is 'twitch', 'youtube', or 'streamhub' depending on what fired the event. Most event types below are Twitch-only; a few (noted per-event) also fire from a connected YouTube account on StreamSync Pro. Filter on e.detail.platform in your overlay JS if you need to treat them differently. Kick never fires overlay events — it has no live event feed.


Event reference

chat.message

Fires for every chat message the bot receives. Twitch only — the bot only reads Twitch IRC (see Chat Bot); YouTube/Kick chat is not wired into the bot engine.

js
window.addEventListener('chat.message', function(e) {
  const { displayName, userName, message, color,
          isMod, isSubscriber, isBroadcaster, bits } = e.detail.data;
});
FieldTypeDescription
messageIdstringTwitch message ID
userIdstringTwitch user ID
userNamestringLogin name (lowercase)
displayNamestringDisplay name (case-preserved)
messagestringRaw message text
colorstringChat name color (hex, e.g. #9146FF)
badgesRecord<string,string>Raw badge set/version pairs
isModboolean
isSubscriberboolean
isBroadcasterboolean
bitsnumberCheer bits (0 if not a cheer)
emotesArray<{id,name,positions}>Raw Twitch emote data
partsArray<Part>Parsed message segments with emote image URLs

parts — inline emote rendering

Each element is one of:

js
{ type: 'text',  content: 'hello ' }
{ type: 'emote', id: '...', name: 'accousHi', url: 'https://...', source: 'bttv' }

source is one of 'twitch', 'bttv', '7tv', or 'ffz'. The built-in Chat Box widget renders parts automatically so emotes appear as inline images. Use parts in custom overlay JS when you want the same behaviour:

js
window.addEventListener('chat.message', function(e) {
  var d = e.detail.data;
  var wrap = document.createElement('span');
  (d.parts || [{ type: 'text', content: d.message }]).forEach(function(p) {
    if (p.type === 'emote') {
      var img = document.createElement('img');
      img.src = p.url; img.alt = p.name; img.title = p.name;
      img.style.height = '1.4em'; img.style.verticalAlign = 'middle';
      wrap.appendChild(img);
    } else {
      wrap.appendChild(document.createTextNode(p.content));
    }
  });
});

follow

js
window.addEventListener('follow', function(e) {
  const { displayName, followedAt } = e.detail.data;
});
FieldType
userIdstring
userNamestring
displayNamestring
followedAtstring (ISO 8601)

subscribe

js
window.addEventListener('subscribe', function(e) {
  const { displayName, tier, isGift } = e.detail.data;
});
FieldTypeValues
userIdstring
userNamestring
displayNamestring
tierstring'1000', '2000', '3000', 'prime'
isGiftboolean

subscribe.gift

js
window.addEventListener('subscribe.gift', function(e) {
  const { gifterName, recipientName, tier, total } = e.detail.data;
});
FieldTypeDescription
gifterIdstring
gifterNamestring
recipientIdstring
recipientNamestring
tierstring'1000', '2000', '3000'
totalnumberCumulative gifts from this user

subscribe.message

Fired when a resub message is posted in chat. Also fires from a connected YouTube account (Pro) on a new channel membership — platform will be 'youtube' and tier/months won't be populated since YouTube has no tier/month-count concept.

FieldType
displayNamestring
tierstring
monthsnumber
messagestring

cheer

Also fires from a connected YouTube account (Pro) for Super Chats and Super Stickers — platform will be 'youtube' and bits holds the equivalent amount reported by YouTube's API.

FieldType
displayNamestring
bitsnumber
messagestring
isAnonymousboolean

raid

Twitch only — YouTube and Kick have no raid equivalent.

FieldType
fromUserNamestring
viewerCountnumber

redeem

Fired when a channel point reward is redeemed. Twitch only — YouTube and Kick have no channel points equivalent.

FieldType
displayNamestring
rewardTitlestring
userInputstring
costnumber

stream.online / stream.offline

Also fires from a connected YouTube account (Pro) — platform will be 'youtube' when an active live broadcast is found/ends. Kick has no online/offline event (the Dashboard polls Kick's live status separately instead — see Connecting Platforms).

js
window.addEventListener('stream.online', function(e) {
  console.log('Stream started at', e.detail.data.startedAt);
});

window.addEventListener('stream.offline', function() {
  console.log('Stream ended');
});

Other events

hype_train.begin, hype_train.progress, hype_train.end, poll.begin, poll.end, prediction.begin, prediction.end, ban, timeout, chat.clear

All Twitch only, and all follow the same e.detail.data pattern. See electron/eventBus.ts for the full payload shapes.


Sending events manually

From the app, you can push a custom event to all connected overlays:

js
// In any future automation or from the DevTools console:
window.streamSync.overlay.sendEvent({ type: 'my-custom-event', data: { foo: 'bar' } });

In the overlay:

js
window.addEventListener('my-custom-event', function(e) {
  console.log(e.detail.data.foo); // 'bar'
});

The Custom Overlay Event trigger action does the same thing from a rule — see Triggers & Actions — so you don't have to open DevTools every time; a raid, a follow, or any other stream event can drive it automatically.


Example: Chat Box

HTML

html
<div id="chat"></div>

CSS

css
#chat {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px;
  height: 100vh;
  justify-content: flex-end;
  overflow: hidden;
  font-family: 'Segoe UI', sans-serif;
  font-size: 15px;
}
.msg { display: flex; gap: 6px; align-items: baseline; }
.msg-name { font-weight: 700; flex-shrink: 0; }
.msg-text { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.8); }

JS

js
window.addEventListener('chat.message', function(e) {
  const { displayName, message, color } = e.detail.data;
  const chat = document.getElementById('chat');

  const div = document.createElement('div');
  div.className = 'msg';
  div.innerHTML = `
    <span class="msg-name" style="color:${color || '#9146ff'}">${displayName}</span>
    <span class="msg-text">${message}</span>
  `;
  chat.appendChild(div);

  // Keep last 30 messages visible
  while (chat.children.length > 30) chat.removeChild(chat.firstChild);
});

Example: Task List (!addtask / !taskdone)

See the chat box example above. A full task list example:

JS (adapted from StreamElements — replace STREAMER_NAME with your display name)

js
const STREAMER_NAME = 'YourName';
let taskList = {};

function Task(user, text, number) {
  this.user = user; this.text = text; this.number = number;
}

window.addEventListener('chat.message', function(e) {
  const data  = e.detail.data;
  const parts = data.message.toLowerCase().split(' ');
  const cmd   = parts.shift();

  if (cmd === '!addtask') {
    if (!(data.displayName in taskList)) taskList[data.displayName] = [];
    const n    = taskList[data.displayName].length + 1;
    const task = new Task(data.displayName, parts.join(' '), n);
    taskList[data.displayName].push(task);
    addTask((task.user === STREAMER_NAME) ? 'streamer' : 'chat', task.text, task.user, task.number);
  }

  if (cmd === '!taskdone') {
    const idx = parseInt(parts[0]);
    if (data.displayName in taskList && !isNaN(idx) && idx <= taskList[data.displayName].length) {
      removeTask((data.displayName === STREAMER_NAME) ? 'streamer' : 'chat', data.displayName, idx);
    }
  }
});

function addTask(type, text, user, n) {
  const el = document.createElement('div');
  el.className = `task ${type}`;
  el.id = `task-${type}-${user}-${n}`;
  el.innerHTML = `<span class="num">${n}</span><span class="name">${user}</span><span class="text">${text}</span>`;
  const container = document.querySelector(`.${type}-tasks`);
  container.style.display = 'flex';
  if (type === 'streamer') container.appendChild(el);
  else container.prepend(el);
}

function removeTask(type, user, n) {
  const el = document.getElementById(`task-${type}-${user}-${n}`);
  if (!el) return;
  el.classList.add('fade-out');
  el.addEventListener('animationend', () => el.remove(), { once: true });
}

Tips

  • Transparent background is already set by default (body { background: transparent; overflow: hidden; }). Add it explicitly in your CSS only if you override body.
  • Font loading — Google Fonts CDN links can be added in the HTML <head> tag.
  • Multiple overlays can be open in OBS at once — each is an independent browser source.
  • Event filtering — listen only for the events you care about. Unhandled events are silently discarded.
  • Persistent state — overlay JS state resets whenever OBS refreshes the source. For state that survives refreshes, you'd need localStorage or a server-side store.
  • Testing — click Open in the overlay editor to preview in your browser. Use browser DevTools to inspect events and debug JS errors.

StreamSync and StreamHub documentation.