Skip to content

Widget Development

WolfwithSword edited this page Jan 8, 2026 · 13 revisions

You can develop your own widgets by either modifying the existing presets or by developing your own, with a few considerations.

For developing your own, you will need to implement certain function names to accept data that is pushed to each widget. You don't need to worry for the internal websocket connection, we take care of that.

Each widget is treated as an isolated iframe within an overlay, and will automatically connect to the localhost websocket.

Structure

You can develop your widgets any way you wish. When imported into SubathonManager, only a single .html file is imported. Any relative imports to media files, .css or .js files will resolve.

For the required functions, and anything important script-wise, it is recommended to embed it in <script> tags in the html file, such that they are loaded properly.

The HTML files do not need to be full document-valid files, but still valid HTML.

We currently do not have a way to inject variable overrides for scripts and that sort of customization, but it is in the plans.

You can import any scripts, css, fonts, etc you want. But only local CSS files will be scanned for variables.

Metadata

In your widget's HTML file, you can apply some metadata at the top to pre-define a few values.

At the moment, only Width and Height are used for widgets, but you can put anything here for documentation as well.

<!--
WIDGET_META
Width:520
Height:130
END_WIDGET_META
-->

JS Variable Injection

However, you can also specify javascript variables to inject.

Any value in the metadata that contains a single . will be attempted to parse into <variablen_name>.<variable_type>, where type must be one of the following:

  • Int
  • String
  • Float
  • StringList
  • EventTypeList
    • A comma separated list of valid event_types (Alternate view). Except Command/Unknown.
    • This is equivalent to StringList, except it validates it against SubathonEventTypes, and can be selected via checkboxes in the UI.
  • Boolean
    • This will be a checkbox in the UI configuration
  • StringSelect
    • A list of strings, comma separated. The user can select one of them as the string value. Default, it will select the first item in the list.
  • Percent
    • An integer clamped between 0 and 100, inclusive.
  • EventTypeSelect
    • Allows selection from a list of EventTypes, but only those which provide a base configurable time/points values.
  • AnyFile, ImageFile, SoundFile, VideoFile
    • Will inject a constant string
    • In the UI, it will either be empty, an absolute system path, or a relative path from the widget. If absolute path, it will have a prefix externalPath/ so the backend can resolve it
    • For setting a default, use NONE for empty, or use a relative path. Do not set a default of an absolute path if you intend to share your widget.

A full list of valid data types can be found here or here under WidgetVariableType.

Any data that follows this pattern in the metadata and is valid will be injected as a const at the start of each widget. If a float or int fails to be validated, it will be treated as a String. Anything else used but not supported will also be treated as a String. If the line starts with a special character/symbol, it will be ignored.

Anything where the value is in all capitals NONE will be treated as an empty string. This is not applicable for EventTypeList.

For example:

<!--
WIDGET_META
Width:520
Height:130
applicableEvents.EventTypeList:TwitchSub,TwitchGiftSub,TwitchCheer
pointsName.String:subpoints
secondsToDisplay.Int:5
//test.String:Hello World
#test2.Float:-4.5
showCompleted.Boolean:True
mySelect.StringSelect:Seconds,Points
myEvent.EventTypeSelect:NONE
dinkDonk.SoundFile:./dinkdonk.mp3
END_WIDGET_META
-->

Will produce

<script>
  const applicableEvents = ["TwitchSub", "TwitchGiftSub", "TwitchCheer"];
  const pointsName = "subpoints";
  const secondsToDisplay = 5;
  const showCompleted = true;
  const mySelect = "Seconds";
  const myEvent = ""; // May be selected in UI. Could be "TwitchSub";
  const dinkDonk = "./dinkdonk.mp3";
</script>

CSS

It is recommended in your CSS files, you specify some variables in a :root tag.

Any and all CSS variables will be able to be configured and overwritten via the UI and injected when in use without modifying the raw files.

ex.

:root {
    --main-background-color: #cecece;
    --text-size: 16px;
}

Functions & Data

The following functions are required only if you want their associated data in your widget:

See here for tables regarding their data.

handleSubathonUpdate

This function will be updated very frequently.

From it, you will get the current seconds and points as they change, lock/unlock status, pause/resume status, and multiplier values.

*When multiplier for seconds or points is 1, then there is no multiplier running.

{
  "type": "subathon_timer",
  "total_seconds":  int, //full seconds to make up whole timer
  "days":  int, // number of days left in timer
  "hours":  int, // remainder hours left in timer after days
  "minutes":  int // remainder minutes left after hours,
  "seconds":  int, // remainder seconds left after minutes
  "total_points":  int,
  "is_paused":  bool,
  "is_locked":  bool,
  "is_reversed": bool,
  "multiplier_points":  float, // 1 = multiplier inactive for points
  "multiplier_time":  float, // 1 = multiplier inactive for time
  "multiplier_start_time": timestamp or null,
  "multiplier_seconds_total": int, // 0 if inactive or no duration set 
  "multiplier_seconds_remaining": int, // 0 if inactive or no duration set or duration ended
  "total_seconds_elapsed": int, // total seconds ever elapsed when unpaused. Will not be correct if ever reset time during reverse subathon.
  "total_seconds_added":  int, // total seconds ever added to timer. Will not be correct if ever reset time during normal subathon.
  "currency": string, // current primary currency
  "rounded_money": double, // rounded (floor) sum of all money donations, converted to primary currency
  "fractional_money": double // rounded to 2 decimal spaces sum of all money donations, converted to primary currency
}

handleSubathonEvent

Whenever an event is processed as having added to the subathon timer successfully, this data is pushed.

{
    "type": "event",
    "event_type":  string, 
    "source":  string,
    "seconds_added": int, // seconds added from this event
    "points_added":  int, // points added from this event
    "user":  string, // User who triggered event, or SYSTEM or AUTO
    "value":  string, // For certain events, can contain useful data, e.g., $ amount for tips, number of bits, tier of sub. 
    "amount": int,  // For gift subs, will be number of subs. Otherwise, usually 1
    "currency": string, // currency of donations, or type such as "sub", "bits"
    "command":  string, // if event_type was a Command, which command
    "event_timestamp": datetime, // timestamp of event triggering
    "reversed": bool // if true, seconds were "removed" during a reverse subathon. Null/False is normal.
}

Types

Notes

For event TwitchHypeTrain, it will send an event with the following:

  • seconds_added, points_added of 0
  • value of end, progress, start (or start ... with multiplier details if it started one).
  • amount will be current hype train level. Progress sends only when level increases.
  • user will be your twitch username

It is intended to be used separately frtom other types of events, to help you know when a hype train is active and its current level. Events are sent regardless of auto-multiplier setting, only start value is modified if enabled.

handleGoalsUpdate

When the goals list updates, either with new goals, changed goals, or removal of points

{
    "type": "goals_list",
    "points": long, // current points of the subathon, or rounded (floor) of sum of real money donations if Money goals_type
    "goals": [
      // ... for each goal in all goals
      {
          "text": string, // goal text
          "points":  long, // points for the goal
          "completed": bool // is it completed based on subathong points?
      }
      // ...
    ],
    "goals_type": string/goals_type //  Points or Money
}

Types

handleGoalCompleted

Whenever a new goal is completed (and unchanged from current list)

{
    "type": "goal_completed",
    "goal_text":  string,
    "goal_points":  long, // points for the goal
    "points": long // current points of the subathon at time of completion, or rounded (floor) of sum of real money donations if Money goals_type
}

handleSubathonDisconnect

Triggers whenever the socket disconnects from SubathonManager.

For reconnections, it will always send initial messages to the other fields, as well as a ping/hello system. So you can simply wait for new data where required.

handleValueConfig

Triggers whenever the subathon values configuration (points, seconds) are updated, either in the UI or remotely via a config patch.

{
  "type": "value_config",
  "data": [
    {
      "eventType": "TwitchSub", // eventType
      "source": "Twitch", // eventSource
      "meta": "1000", // meta, will be empty string for most
      "seconds": 60, // double
      "points": 1 // double
    },
    {
      "eventType": "TwitchSub",
      "source": "Twitch",
      "meta": "2000",
      "seconds": 120, // double
      "points": 2 // double
    }
    // ... for each 
  ]
}

Redistribution

As a widget developer, you have full rights to your developed widget(s) and associated files. You are free to redistribute your widgets, commercially or not, as you wish.

We wish to provide SubathonManager as an open ecosystem to enable creatives, artists, hobbyists, content-creators, and more. While the program itself is non-commercial and should not be redistributed, widgets are perfectly OK to be!

Although, in the spirit of this project and in support of creatives and developers, we ask you to not use AI to assist in development of your widgets.

Clone this wiki locally