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:
| Attribute | Effect |
source | Required. The widget module to load. |
id | The element id the widget renders into. |
class, style | Copied onto the created element. |
element | What to create: a div by default, or canvas, or none for a widget with no DOM. |
clear-on-reset | Dispose this widget when the session resets, rather than keeping it. |
data-* | Copied through to the created element. |
| anything else | Arrives 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.
Every widget extends Widget, from /js/widgets/widgets.js.
The base provides the lifecycle:
| Member | Purpose |
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.
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.
- 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.