Starship Horizons
Log In
☰
Knowledge Base›Widgets

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. Sets this.Disposed, releases subscriptions and packets, then removes the widget's own element.
this.DisposedTrue once the widget has been disposed. A widget that drives itself with requestAnimationFrame checks it.
this.ControllerThe widget controller that built this widget.

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.

A loop that queues itself must check this.Disposed. A render that calls requestAnimationFrame on itself has no subscription for disposal to release, so without the check a disposed radar or HUD goes on drawing into a canvas that is no longer on the page.

Removing a widget removes everything inside it. Every hz-widget in a widget's layout is a separate instance. When a widget is removed through the controller (Remove or RemoveByID), the controller also disposes every widget mounted inside its element, newest first, so children go before their parent. What counts as inside is judged by the element the controller built for each widget, not by its Parent. A session reset is different: it disposes only the widgets marked clear-on-reset, and leaves what they contain alone.

A host that replaces its own contents must clear them first. If a widget fetches a layout and then writes it into its own element, anything another widget mounted there in the meantime is wiped from the page but keeps running. Call this.Controller?.RemoveNested?.(this) before the write, and check that the widget was not itself disposed while the layout was on its way.

Reset is not startup

The single most common class of widget bug. A session reset re‑runs initialization 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.
  • Attached by screen tag — a module's <screen-widgets> list names widgets with a tags attribute, and the server adds each one to every screen whose registration carries a matching tag. See Client Architecture.
  • Pushed by the Game Master — a GM can send a widget to a specific console during play.

A screen widget list is an XML file in a module's World folder:

<resource fileversion="1.0">
    <screen-widgets>
        <widget id="convention-list-main" source="convention-list" tags="main-viewer">Main Viewer Mission Details</widget>
    </screen-widgets>
</resource>

To see every widget the server can serve, which module's copy wins, and which modules it overrides, read api/widget-sources.

To write one, see Writing A Widget.

Last updated 19 October 2026