Skip to content
TipPage Docs
Esc
navigateopen⌘Jpreview
On this page

Custom widgets

Build your own overlay widgets with HTML, CSS, and JS - StreamElements custom widgets run as-is.

A custom widget is a widget you write yourself: an HTML pane, a CSS pane, a JS pane, and a JSON schema describing the settings it exposes. It runs on your overlay like any built-in widget - drag it, resize it, stack it, test it from the editor’s emulator - and it receives your tips, follows, subs, cheers, raids, and chat as events the moment they happen.

The runtime is StreamElements-compatible: widgets written for SE’s custom widget system - onWidgetLoad, onEventReceived, SE_API, {{field}} templates, jQuery - run here without changes. If you’ve got a widget from an SE gallery or a commission, paste its four panes in and it works. (A few SE surfaces are stubbed for now - see compatibility below for the list.)

Quick start

Add a Custom widget

In the overlay editor, Add widget → Custom widget. You can have as many custom widgets as you like, each with its own settings.

Create or pick the code

With the widget selected, the inspector shows a Widget picker. Choose an existing widget from your library, or hit New widget to open the code editor: name, HTML, CSS, JS, and Fields (JSON).

Fill in the fields

Whatever the widget declares in its fields schema appears as a normal settings form in the inspector - text boxes, sliders, colour pickers, image uploads. Each placed instance keeps its own values, so the same widget can run twice with different settings.

Test it

Open Test alerts in the bottom bar and fire tips, follows, subs, cheers, raids, and chat messages at it. What you see in the editor is exactly what OBS renders.

The four panes

  • HTML - the widget’s markup. {{fieldName}} (or {fieldName}) placeholders are replaced with field values before it renders.
  • CSS - styles, same templating. The widget’s box is the rectangle you draw in the editor; html, body inside it are transparent and sized to fill it.
  • JS - your logic. jQuery is preloaded (like StreamElements), and {{fieldName}} templates are substituted here too (single-brace {name} is left alone in JS - that’s code, not a template).
  • Fields - a JSON object describing your settings form. See below.

Fields

The fields schema is the SE format: each key is a field, each value describes it.

{
  "titleText": { "type": "text", "label": "Title", "value": "Hello chat" },
  "accent":    { "type": "colorpicker", "label": "Accent colour", "value": "#4ade80" },
  "speed":     { "type": "slider", "label": "Speed", "value": 5, "min": 1, "max": 20, "step": 1 },
  "showIcon":  { "type": "checkbox", "label": "Show icon", "value": true },
  "mode":      { "type": "dropdown", "label": "Mode", "value": "bar", "options": { "bar": "Bar", "ring": "Ring" } },
  "logo":      { "type": "image-input", "label": "Logo" },
  "font":      { "type": "googleFont", "label": "Font", "value": "Poppins" },
  "reset":     { "type": "button", "label": "Reset counter", "value": "reset" },
  "widgetName": { "type": "hidden", "value": "My Counter" }
}

Supported types and how the inspector renders them:

Type Inspector control
text, sound-input, video-input Text input
number, slider Number field (with min / max / step)
checkbox Toggle
colorpicker Colour picker
dropdown Select built from options
googleFont Font picker
image-input Upload button - files go to TipPage’s CDN, same as image widgets
button A button; clicking it fires a widget-button event into the widget
hidden Not shown; the default value is passed straight through

Add a "group" property to any field to section the form under a heading. Field values reach your JS as obj.detail.fieldData in onWidgetLoad, and as {{templates}} in all three panes.

The runtime

onWidgetLoad

Fires once when the widget mounts:

window.addEventListener('onWidgetLoad', function (obj) {
  const fieldData = obj.detail.fieldData;   // your fields, typed (numbers, booleans, strings)
  const currency  = obj.detail.currency;    // { code, name, symbol }
  const channel   = obj.detail.channel;     // { username, apiToken: "" }
  const session   = obj.detail.session.data; // SE-shaped session snapshot
});

onEventReceived

Fires on every live event. obj.detail.listener tells you what it is:

Listener Fired when Event payload
tip-latest A tip alert plays { name, amount, message }
follower-latest New follow { name }
subscriber-latest Sub, resub, or gift { name, amount, tier, message, gifted?, sender?, bulkGifted? } - amount is months (or gift count), tier is 1/2/3
cheer-latest Bits cheered { name, amount, message }
raid-latest Incoming raid { name, amount } - amount is viewers
message A chat message { data: { displayName, displayColor, userId, badges, emotes, text, msgId, ... } }
delete-message A mod deletes one message { msgId }
delete-messages A user is banned/timed out { userId }
kvstore:update Any widget writes to the store { data: { key: "customWidget.<key>", value } }
widget-button A button field is clicked in the editor { field, value }

Every overlay keeps one shared Twitch chat connection open, whether or not the chat widget is on the layout - so message events always arrive. A pasted SE chat box works on an otherwise-empty overlay.

SE_API

Call What it does here
SE_API.store.set(key, value) Persists to a per-widget key/value store on TipPage; fans out kvstore:update to every custom widget
SE_API.store.get(key) Promise of the stored value (null if unset)
SE_API.getOverlayStatus() { isEditorMode, muted } - live mute state in the editor
SE_API.sanitize({ message }) Resolves the message unchanged (skip: false)
SE_API.cheerFilter(message) Strips cheermote tokens
SE_API.setField(key, value) Updates local fieldData (not persisted)
SE_API.counters.get(name) Resolves { counter, value: 0 } - TipPage has no bot counters yet
SE_API.resumeQueue() Accepted, currently a no-op

Store limits: 100 keys per widget, 10KB per value.

StreamElements compatibility

Identical today: the event and load lifecycle, listener names, payload shapes, SE_API.store, fields schema and form rendering, {{templates}}, jQuery, per-widget sandboxing.

Known gaps, so you’re never surprised:

  • session.data has the right shape but zeroed totals (latest/recent and week/month/total counters). Widgets render; running totals start at 0.
  • SE_API.counters always returns 0 (no bot counters yet).
  • SE_API.resumeQueue() and the widgetDuration hidden field don’t hold TipPage’s alert queue - alerts are played by the built-in alert widget.
  • sound-input / video-input fields are plain URL inputs for now.

Security

Custom widget code runs inside a sandboxed iframe (sandbox="allow-scripts", no same-origin). Concretely, that means pasted code cannot:

  • read or touch the rest of the overlay (other widgets, the page DOM);
  • see your overlay URL/key or the overlay session token;
  • call the TipPage API as you - it has no cookies, no tokens, and channel.apiToken is deliberately empty;
  • break other widgets - worst case it wedges its own box.

Its only communication is a fixed set of messages the overlay defines (events in, store calls out). Two things to still be deliberate about:

  • Only the overlay owner (and super admins) can create or edit widget code. Team members with overlay access can place widgets and change their fields, but never the code - because the code ultimately executes inside your OBS.
  • Sandboxed code can still load external resources and make its own network requests (images, fonts, its own APIs - many SE widgets rely on this). Treat pasting a widget like installing software: use code you trust or can read. The sandbox limits the blast radius to the widget’s own box and whatever events you feed it.

Privacy

What widget code can observe is exactly what your overlay already shows on stream: donor display names, tip amounts and messages, follower/sub/cheer/ raid names, and public chat messages. It never receives emails, payment details, real names, session tokens, or anything from your dashboard.

The code itself runs locally - in your OBS browser source and in the editor preview - not on TipPage’s servers. The only widget data TipPage stores is what you put in it: the code you save, the field values on your layout, and whatever the widget writes to its own key/value store. All of it is scoped to your account, and deleting a widget deletes its store.

Because pasted code can call external services, a malicious widget could send the events it sees (donor names, chat) to a third party. That’s the same trust model as any browser-source widget on any platform - it’s why code editing is owner-only, and why you should paste from sources you trust.

Limits

Thing Limit
Widgets per account 100
Each code pane (HTML / CSS / JS) 200KB
Fields schema 50KB
Store keys per widget 100, 10KB per value
Field value on a layout 2,000 characters

Was this page helpful?