This guide documents the architecture and tactics used to build the Unified Goal Widget. It demonstrates how to create a robust, production-ready widget that runs seamlessly in both a Local Development Environment and the StreamElements OBS/Browser Source using a single codebase.
The core philosophy is strict separation of Logic (JS), Presentation (CSS/SVG), and Configuration (JSON), bridged by an Environment Adapter.
StreamElements widgets normally require being uploaded to the dashboard to test functionality. This feedback loop is very slow.
We detect the runtime environment. If the global SE_API object is missing, we assume we are running locally and inject a Mock Layer that simulates StreamElements behavior.
/* widget.js Pattern */
(function() {
// 1. Detect Environment
if (typeof SE_API !== 'undefined') {
// Production: Attach real listeners
window.addEventListener('onWidgetLoad', (obj) => init(obj.detail.fieldData, obj.detail.session.data));
window.addEventListener('onEventReceived', (obj) => onEvent(obj.detail));
} else {
// Local Dev: Inject Mocks
window.isDev = true;
mockStreamElementsEnvironment();
}
})();We rely on three specific interaction points.
Defines the UI controls in the StreamElements dashboard (left sidebar).
- Tactic: Group related fields using the
"group"property (e.g., "Colors", "Behavior"). - Tactic: Use distinct
types (colorpicker,image-input,slider,dropdown) to create a professional UX. - Tactic: Provide rational
valuedefaults so the widget looks good immediately upon loading.
Called once when the widget starts or when settings change.
- Payload: Contains user settings (
fieldData) and session data (data). - Tactic: Strict Defaulting. StreamElements might return
undefinedfor new fields. Always merge with a default object:settings = { ...defaults, ...fieldData };
- Tactic: Type Safety. Convert string inputs to numbers immediately using
parseFloat()to preventNaNerrors later.
Called whenever a Twitch/YouTube event (Follow, Sub, Tip) occurs.
- Tactic: Early Return. Filter events immediately if they don't match the listener you care about.
- Tactic: Unified Handlers. Route different event types (Tip, Cheer, Sub) to a single
updateProgress(amount)function to keep logic clean.
To make the widget work locally without changing code, we simulate the environment at the bottom of widget.js.
Features of the Mock:
- Simulated Store: A dummy
SE_API.storeobject that implementsgetandset(returning Promises) so the main code doesn't crash. - Dev UI: An HTML overlay (
<div id="dev-controls">) that injects fake events when buttons are clicked. - Auto-Init: We manually call the
init()function with a preset configuration object after100ms, mimicking the boot sequence of the real widget.
Instead of using static images or CSS border-radius hacks, we used SVG Paths with stroke-dasharray.
- Why: Infinite scalability, zero pixelation, and performance.
- The Power Move (
describeArc): A single mathematical function calculates the SVG path command (d="M... A...") dynamically based on start and end angles.- This allowed us to reuse the exact same code for "Full Circle", "Arch", "Horseshoe", and "Wide Arc" just by changing the angles passed to the function.
We use SE_API.store.set('key', value) to save the current goal progress.
- Why: Browser sources in OBS refresh frequently. Without persistence, the goal resets to 0 every time the streamer restarts OBS.
- Key Isolation: Use unique keys (e.g.,
goal_unified_current) to avoid conflicts if the user runs multiple widgets.
We map fields.json settings directly to CSS variables (--primary, --size, --font).
- Benefit: changing a color in the settings updates the DOM instantly via the
styleattribute on the root, without needing to query selectors or force expensive style recalculations on individual elements.
This structure allows you to zip the contents and upload/copy-paste effortlessly.
/unified-widget
βββ widget.html (Structure + Hidden Dev UI)
βββ widget.css (Styles + CSS Vars + Dev UI Styles)
βββ widget.js (Logic + Mock Adapter)
βββ fields.json (Schema configuration)
StreamElements Custom Widget Development Masterclass
This comprehensive guide documents the architecture, tactics, and best practices used to build the Unified Goal Widget. It serves as a blueprint for creating robust, production-ready widgets that work seamlessly in both Local Development Environments and StreamElements OBS/Browser Sources.
ποΈ 1. Architecture: The "Hybrid" Bridge Pattern
The core challenge in StreamElements development is the feedback loop. Uploading code to the dashboard to test every small change is inefficient. We solve this using a Hybrid Architecture that abstracts the environment.
The Problem
window.onEventReceived. Has a globalSE_APIobject.localhost. Has no event stream. Has noSE_API.The Solution
We detect the runtime environment and inject a Mock Adaptor if we are running locally. This allows the exact same business logic to run in both places without modification.
Code Pattern
ποΈ 2. Mastering
fields.jsonThe
fields.jsonfile is the contract between your code and the user. It generates the configuration UI in the StreamElements dashboard.Field Types Reference
{"type": "header", "label": "Colors"}{"type": "text", "label": "Title", "value": "GOAL"}{"type": "number", "step": 1, "min": 0, "max": 100}{"type": "checkbox", "label": "Enable Sound"}{"type": "colorpicker", "value": "#ff0000"}{"type": "dropdown", "options": {"key": "Label"}}{"type": "slider", "min": 0, "max": 100, "step": 1}{"type": "image-input", "label": "Custom Icon"}{"type": "googleFont", "label": "Font Family"}Best Practices
"group"property on every field to organize them into tabs (e.g.,Settings,Style,Data).π 3. The SVG Engine:
describeArcDirect CSS manipulation is limited for complex shapes. We use SVG paths because they are vector-based (crisp at any resolution) and mathematically manipulatable.
The Algorithm
To draw any circular segment (Circle, Arch, Horseshoe), we use a helper that converts Polar Coordinates (Angles) to Cartesian Coordinates (X, Y).
Shape Recipes
By simply changing the angles passed to this function, we create different widgets:
πΎ 4. State & Persistence
Browser sources in OBS are ephemeral. If the user closes OBS or hides the scene, the memory is cleared. You must persist data.
The
SE_API.storeStreamElements provides a cloud key-value store.
SE_API.store.get('my_key_name').then(val => { ... })SE_API.store.set('my_key_name', value)Strategy
null, use thestartingValuefromfields.json..set()the new value.goal_unified_v1) to prevent collisions with other widgets I might have.β‘ 5. CSS Architecture
We use CSS Custom Properties (Variables) to make the styling entirely data-driven.
The Variables
The JS Bridge
In
widget.js,applyStyles()maps the JSON settings to these variables:This allows for instant, repaint-free updates. Changing the size in the JS simply updates one number, and the entire widget (CSS layout, SVG dimensions) scales relative to that variable.
π¨ 6. Troubleshooting Guide
fields.jsonvalue was empty or invalid.stroke-dashoffsetset to full length.offset: 0.z-index: 1, Textz-index: 2in CSS.<link>tag dynamically forsettings.fontFamily..mode-semi) to adjust top/bottom spacing.π 7. Deployment Checklist
Before handing off code:
widget.jsdefaults matchfields.jsondefaults..hidden { display: none })..html,.css,.js,.json) into the StreamElements Editor fields.