> ## Documentation Index
> Fetch the complete documentation index at: https://docs.1up.vision/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Widgets

> Build custom overlay widgets with HTML, CSS, JavaScript, StreamElements compatibility, and the Vision JavaScript API.

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

<Steps>
  <Step title="Open an overlay">
    Go to the Vision overlay editor.
  </Step>

  <Step title="Add a Custom Widget layer">
    Add a new layer and choose **Custom Widget**.
  </Step>

  <Step title="Open the editor">
    Select the layer and click **Open Custom Widget Editor**.
  </Step>

  <Step title="Add your code">
    Fill the **HTML**, **CSS**, **JS**, **Fields**, and **Data** tabs.
  </Step>
</Steps>

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.     |

***

## 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 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.

```json theme={null}
{
  "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:

```html theme={null}
<div class="title">{title}</div>
```

```css theme={null}
.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:

```js theme={null}
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

```js theme={null}
{
  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.

```js theme={null}
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`:

```js theme={null}
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.

```js theme={null}
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

```js theme={null}
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.

```js theme={null}
const label = await Vision.variables.interpolate("Current score: {score}");
```

### Subscribe to variable changes

```js theme={null}
Vision.variables.onChange((variable) => {
  console.log(variable.key, variable.text);
});

Vision.variables.subscribe("score", ({ text }) => {
  document.querySelector("#score").textContent = text;
});
```

### Variable object

```js theme={null}
{
  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"
}
```

<Note>
  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.
</Note>

***

## StreamElements Compatibility

Imported StreamElements widgets can keep using `window.SE_API` and StreamElements event names.

```js theme={null}
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

```js theme={null}
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.

```js theme={null}
if (window.Vision) {
  Vision.on("ready", initVisionWidget);
} else {
  window.addEventListener("onWidgetLoad", initStreamElementsWidget);
}
```

***

## Minimal Vision Widget

HTML:

```html theme={null}
<div class="card">
  <div id="title"></div>
  <div id="score"></div>
</div>
```

CSS:

```css theme={null}
.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:

```js theme={null}
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;
});
```
