Custom Widgets are overlay layers that run HTML, CSS, and JavaScript inside a sandboxed iframe. They are designed for two use cases:
- Importing existing StreamElements custom widgets with minimal changes.
- Building new widgets directly for Vision with
window.Vision.
Use the Vision API for new widgets. Use StreamElements compatibility only when importing existing StreamElements widgets.
Open an overlay
Go to the Vision overlay editor.
Add a Custom Widget layer
Add a new layer and choose Custom Widget.
Open the editor
Select the layer and click Open Custom Widget Editor.
Add your code
Fill the HTML, CSS, JS, Fields, and Data tabs.
The editor has five tabs:
| Tab | Purpose |
|---|
| HTML | Markup inserted into the widget body. |
| CSS | Styles inserted into the widget document. |
| JS | JavaScript executed after the HTML is added. |
| Fields | StreamElements-style field definitions. |
| Data | Current field values used by the widget. |
Use Import files or ZIP to drag in multiple files or a single zip. Vision detects each file’s purpose by its filename and extension.
| Imported file | Detected as |
|---|
html.txt, chat.html, names containing html | HTML |
style.css, main.css, names containing css or style | CSS |
widget.js, script.js, names containing js, javascript, or script | JS |
fields.json, field.json, plain widget JSON files | Fields |
data.json, fielddata.json | Data |
If a zip contains multiple possible matches, explicitly named files like fields.json, html.txt, or style.css win over generic helper files.
Build Guidelines
Custom Widgets should be self-contained. Put the rendered structure in HTML, visual styling in CSS, and behavior in JS.
- Use
Vision.on("ready", callback) before reading Vision data.
- Keep widget layout inside the layer size selected in the overlay editor.
- Use transparent backgrounds unless the widget intentionally needs a panel or card.
- Load external scripts only when you need them.
- Prefer
window.Vision for new widgets and window.SE_API only for imported StreamElements widgets.
- Use Fields for creator-facing configuration instead of hardcoding labels, colors, text, or asset URLs.
- Use Vision Variables for dynamic overlay state such as scores, counters, timers, telemetry, or API/webhook data.
Fields and Data
The Fields tab uses StreamElements-style field JSON. Vision generates editor controls from this JSON and stores the current values in the Data tab.
Supported field types:
text
number
slider
checkbox
dropdown
colorpicker
hidden
button
image-input
Field groups are collapsed by default in the layer settings panel.
{
"title": {
"type": "text",
"label": "Title",
"value": "Latest follower",
"group": "Content"
},
"accentColor": {
"type": "colorpicker",
"label": "Accent color",
"value": "#8b5cf6",
"group": "Design"
},
"testAlert": {
"type": "button",
"label": "Test alert",
"value": "test-alert",
"group": "Testing"
}
}
Use field values in HTML, CSS, or JS with token replacement:
<div class="title">{title}</div>
.title {
color: {accentColor};
}
Vision API
New widgets should use window.Vision. The Vision API is cleaner than StreamElements compatibility mode and exposes Vision-specific data such as overlay variables.
Start with the ready event:
Vision.on("ready", async ({ channel, fields, variables }) => {
document.querySelector("#title").textContent =
fields.title || `Live on ${channel.displayName}`;
const score = await Vision.variables.get("score");
document.querySelector("#score").textContent = String(score ?? "0");
});
Ready payload
{
vision: {
version: "1.0.0",
runtime: "custom-widget"
},
widget: {
id: "layer-id",
name: "Custom Widget",
width: 800,
height: 600
},
overlay: {
id: "overlay-id",
name: "Main Overlay",
isEditor: false
},
account: {
id: "account-id"
},
channel: {
id: "channel-name",
username: "channel-name",
displayName: "Channel Name",
provider: "twitch"
},
fields: {},
variables: {
all: [],
byKey: {}
},
session: {}
}
Vision Events
Use Vision.on, Vision.once, and Vision.off to work with events.
const unsubscribe = Vision.on("variables.change", (variable) => {
console.log(variable.key, variable.text);
});
Vision.once("ready", (context) => {
console.log(context.overlay.name);
});
unsubscribe();
The same methods are also available on Vision.events:
Vision.events.on("variables.change", callback);
Vision.events.once("ready", callback);
Vision.events.off("ready", callback);
Current built-in Vision events:
| Event | Payload |
|---|
ready | Full widget context. |
variables.snapshot | Full variables snapshot. |
variables.change | One resolved variable that changed. |
Vision Context
Use Vision.context.get() to read the latest context at any time.
const context = await Vision.context.get();
console.log(context.widget.id);
console.log(context.overlay.isEditor);
console.log(context.fields);
console.log(context.variables.byKey.score);
Vision Variables
Vision overlay variables are available to Custom Widgets through Vision.variables.
Variables include global variables and variables scoped to the current overlay. If a global and overlay-scoped variable use the same key, the overlay-scoped variable wins.
Variable values are resolved the same way they are for text chips and score bindings. This includes manual variables, counters, time variables, mobile telemetry, channel stats, quiz show variables, tournament variables, API variables, sheets variables, webhooks, expressions, Spotify variables, and extension variables.
Read variables
const all = await Vision.variables.list();
const rawScore = await Vision.variables.get("score");
const scoreText = await Vision.variables.text("score");
const fullScore = await Vision.variables.resolve("score");
All methods accept either the variable key or variable id.
Interpolate variables
Use interpolate to replace {key} or {{key}} tokens with formatted variable text.
const label = await Vision.variables.interpolate("Current score: {score}");
Subscribe to variable changes
Vision.variables.onChange((variable) => {
console.log(variable.key, variable.text);
});
Vision.variables.subscribe("score", ({ text }) => {
document.querySelector("#score").textContent = text;
});
Variable object
{
id: "variable-id",
key: "score",
label: "Score",
description: null,
scope: "overlay", // or "global"
valueType: "number",
source: "counter",
value: 12,
text: "12",
pending: false,
updatedAt: "2026-05-26T12:00:00.000Z"
}
Variables are read-only in Custom Widgets right now. Widgets can read, render, interpolate, and subscribe to variables, but cannot mutate manual or counter variables from a public overlay source.
StreamElements Compatibility
Imported StreamElements widgets can keep using window.SE_API and StreamElements event names.
window.addEventListener("onWidgetLoad", (event) => {
const { fieldData, channel } = event.detail;
console.log(fieldData, channel.username);
});
window.addEventListener("onEventReceived", (event) => {
if (event.detail.listener === "message") {
console.log(event.detail.event.data.displayName);
}
});
StreamElements events
| Event | Description |
|---|
onWidgetLoad | Fires when the widget is ready. |
onEventReceived | Fires for chat, activity, store, and editor test events. |
onSessionUpdate | Fires when session data changes after activity events. |
SE_API methods
await SE_API.store.set("counter", 1);
const counter = await SE_API.store.get("counter");
Supported compatibility methods:
SE_API.store.get(key)
SE_API.store.set(key, value)
SE_API.counters.get(counter)
SE_API.sanitize(payload)
SE_API.cheerFilter(message)
SE_API.getOverlayStatus()
SE_API.resumeQueue()
SE_API.setField(key, value, reload)
SE_API.store values are global per 1UP account.
Recommended API Pattern
Build new widgets against Vision. Keep StreamElements code unchanged when importing existing widgets. If a widget needs to support both environments, detect the API at runtime.
if (window.Vision) {
Vision.on("ready", initVisionWidget);
} else {
window.addEventListener("onWidgetLoad", initStreamElementsWidget);
}
HTML:
<div class="card">
<div id="title"></div>
<div id="score"></div>
</div>
CSS:
.card {
padding: 24px;
border-radius: 18px;
color: white;
background: linear-gradient(135deg, #7c3aed, #ec4899);
font-family: Inter, system-ui, sans-serif;
}
#title {
font-size: 18px;
opacity: 0.8;
}
#score {
font-size: 56px;
font-weight: 800;
}
JS:
Vision.on("ready", async ({ fields }) => {
document.querySelector("#title").textContent = fields.title || "Score";
document.querySelector("#score").textContent =
await Vision.variables.text("score");
});
Vision.variables.subscribe("score", ({ text }) => {
document.querySelector("#score").textContent = text;
});