Skip to main content
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.

Create a Custom Widget

1

Open an overlay

Go to the Vision overlay editor.
2

Add a Custom Widget layer

Add a new layer and choose Custom Widget.
3

Open the editor

Select the layer and click Open Custom Widget Editor.
4

Add your code

Fill the HTML, CSS, JS, Fields, and Data tabs.
The editor has five tabs:
TabPurpose
HTMLMarkup inserted into the widget body.
CSSStyles inserted into the widget document.
JSJavaScript executed after the HTML is added.
FieldsStreamElements-style field definitions.
DataCurrent field values used by the widget.

Import Existing Widgets

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 fileDetected as
html.txt, chat.html, names containing htmlHTML
style.css, main.css, names containing css or styleCSS
widget.js, script.js, names containing js, javascript, or scriptJS
fields.json, field.json, plain widget JSON filesFields
data.json, fielddata.jsonData
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:
EventPayload
readyFull widget context.
variables.snapshotFull variables snapshot.
variables.changeOne 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

EventDescription
onWidgetLoadFires when the widget is ready.
onEventReceivedFires for chat, activity, store, and editor test events.
onSessionUpdateFires 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.
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);
}

Minimal Vision Widget

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;
});