Starship Horizons
Log In
Knowledge BaseWidgets

Widgets

A widget is a self‑contained piece of console UI with its own JavaScript class. Nearly everything on a console is one — the radar, the shield display, the contact list, the waypoint tabs, the comms panel. There are around a hundred in the base game.

Widgets are the reason consoles are not fixed layouts. A component installed on a ship can declare that a console should show a widget, and it appears without anyone editing that console. See Components.

Declaring one

In a page, a widget is a custom element:

<hz-widget id="speed-container" source="speed" class="panel-container"></hz-widget>

source names the module — source="speed" loads /js/widgets/speed.js. Everything else is passed through:

AttributeEffect
sourceRequired. The widget module to load.
idThe element id the widget renders into.
class, styleCopied onto the created element.
elementWhat to create: a div by default, or canvas, or none for a widget with no DOM.
clear-on-resetDispose this widget when the session resets, rather than keeping it.
data-*Copied through to the created element.
anything elseArrives in the widget's options. This is how vesselrole, group, tag, viewmode and the rest are passed.

How mounting works

The widget controller scans the document for hz-widget elements, replaces each with the real element, imports the module if it has not already been loaded, and constructs the class. The module's default export is the constructor — the class name itself does not matter.

The scan runs when the page becomes ready and again whenever something asks for it, so widgets added to the DOM later are picked up. Widgets that arrive before their module has finished importing are queued and constructed when it lands.

Duplicate ids mount twice. Two hz-widget declarations sharing an id create two instances, each with its own loops and subscriptions, while getElementById hands both the same element — so one silently drives the other's canvas. The controller warns about this in the console rather than picking one, because which declaration was intended is a layout decision. If a console is mysteriously heavy, look for this first.

The Widget base class

Every widget extends Widget, from /js/widgets/widgets.js. The base provides the lifecycle:

MemberPurpose
this.Subscribe(topic, fn)Subscribe to a broadcast topic, remembering it so disposal can release it.
this.AcceptPacket(id)Claim a server packet on this widget's behalf.
this.RejectPacket(id)Drop a claim early, without disposing.
Reset()Called on session reset. Override it.
Resize()Called when the console resizes. Override it.
Dispose()Tear down. Releases subscriptions and packets, then removes the element.

The lifecycle rules that actually matter

These are not style preferences. Each one exists because breaking it caused a real leak.

Use this.Subscribe, never game.Subscribe. The global has no counterpart, so a widget that uses it leaves its handlers running permanently — still firing against an object that was disposed. A console left open across several mission restarts accumulates them. game.Subscribe is correct only for page‑level code and singletons that live as long as the document.

Use this.AcceptPacket, not the global. Packet ownership is reference‑counted across widgets: several widgets want the same packet, and one packet feeds several topics. The socket only tells the server to stop sending when the last owner lets go. Claiming through the global means never letting go.

If you override Dispose(), call super.Dispose(). The base's body is what performs the release. Overriding it with a copy of the base's code — which several widgets once did — silently skips it.

A constructor that throws still cleans up, but only just. The controller tracks partially built widgets so that one which throws half‑way has its registrations taken back. Do not rely on it as a design; a widget whose constructor can fail should fail before it registers anything.

Reset is not startup

The single most common class of widget bug. A session reset re‑runs initialisation on a page that has already been running — it is not a fresh load. Anything a widget does on the way up has to be safe to do again: creating GPU resources, adding scene objects, starting loops, attaching listeners. A widget that assumes it is only ever constructed once will leak once per mission restart, and the symptom is a console that is fine for one session and slow by the fourth.

Finding a widget at runtime

Every mounted widget is reachable from the page:

workbench.Widgets.Find("Radar3D")        // by Name
workbench.Widgets.FindByID(7)            // by widget id

Find matches the Name the widget gave itself in its constructor, which by convention includes its element id — "Heading: #heading". This is how console pages reach into their own widgets to wire a slider to a 3D radar, and it is a genuinely useful debugging handle from the browser console.

Where widgets come from

  • Declared in a screen file — the console's fixed furniture.
  • Declared by a component — appears when that component is installed. See Components.
  • Declared by a vessel class or a mission — both have a Widgets section.
  • Pushed by the Game Master — a GM can send a widget to a specific console during play.

To write one, see Writing A Widget.

Last updated 5 September 2026