πΊπΈ English | πͺπΈ EspaΓ±ol
HaxBall UI Framework is a minimal, stable UI layer for building overlay windows on top of the HaxBall client.
It is not a full framework like React. It is not a game engine. It is a small, well-designed UI core that gives you full DOM control without fighting HaxBall.
npm install haxball-ui-framework// ES Module (Vite, Webpack, Rollup)
import HaxUI from 'haxball-ui-framework';
// CommonJS (Node.js)
const HaxUI = require('haxball-ui-framework');Or inject the IIFE bundle directly into HaxBall (no npm needed):
// Paste dist/haxball-ui-framework.iife.js into the DevTools console
// or @require it from a Tampermonkey userscript
HaxUI.diagnostics(); // β { initialized: false, version: 'v1', ... }HaxBall UI Framework introduces a thin overlay layer over the HaxBall DOM that enables:
- window creation and management
- dynamic content updates
- clean lifecycle and destruction
- full CSS isolation via Shadow DOM
- safe event handling that doesn't leak into the game
- drag & resize, native-styled buttons, and a theme that matches HaxBall's own UI
Everything is built around a single principle:
One API. One namespace. Full control.
| Layer | Module | Responsibility |
|---|---|---|
| Bootstrap | RootMount |
Detects execution context, anchors #haxui-root to document.body, re-anchors if HaxBall clears the DOM |
| Registry | WindowManager |
Window registry, lifecycle, and z-index stack |
| Instance | Window |
Builds the DOM tree for one window, owns its Shadow Root |
| Safety | EventGuard |
Per-event-type policy so window events never leak into the game |
| Safety | EventRegistry |
Tracks every listener so destroy() cleans up with zero leaks |
| Style | StyleManager |
Injects base styles per theme into each Shadow Root |
| Content | Sanitize |
DOMParser-based sanitization β strings never hit innerHTML raw |
| Interaction | DragManager |
Drag windows by their header, clamped to the viewport |
| Interaction | ResizeManager |
Resize from any of the 4 corners, with minimum dimensions |
| Integration | ButtonInjector |
Injects native-styled buttons into HaxBall's own button bar |
| Public API | HaxUI |
window.HaxUI β the only global name exposed |
| Handle | WindowHandle |
Lightweight object returned per window β no internals exposed |
haxball-ui-framework/
β
βββ src/ # ES Module source (import/export)
β βββ index.js
β βββ constants/config.js
β βββ utils/sanitize.js
β βββ core/
β βββ HaxUI.js
β βββ WindowManager.js
β βββ Window.js
β βββ RootMount.js
β βββ EventGuard.js
β βββ EventRegistry.js
β βββ StyleManager.js
β βββ DragManager.js
β βββ ResizeManager.js
β βββ ButtonInjector.js
β
βββ dist/ # Built outputs (generated by npm run build)
β βββ haxball-ui-framework.esm.js # ES Module
β βββ haxball-ui-framework.cjs.js # CommonJS
β βββ haxball-ui-framework.iife.js # IIFE β paste into HaxBall console
β
βββ core/ # Legacy IIFE source (kept for build:bundle)
βββ constants/
βββ utils/
β
βββ dev/
β βββ playground.js # 13 test groups, 70+ assertions
β βββ examples.js # 7 worked examples
β
βββ build-esm.cjs # Builds dist/ (ESM + CJS + IIFE)
βββ build.cjs # Builds legacy haxball-ui.bundle.js
βββ package.jsonnpm install haxball-ui-frameworkimport HaxUI from 'haxball-ui-framework';
HaxUI.init();
const win = HaxUI.createWindow({
id: 'stats',
title: 'Statistics',
theme: 'haxball',
width: 260,
height: 180,
x: 16,
y: 16,
content: '<p>Loading...</p>'
});npm run build
# β dist/haxball-ui-framework.iife.jsPaste into the HaxBall DevTools console or add to a Tampermonkey userscript with @require file:///path/to/haxball-ui-framework.iife.js.
git clone https://github.com/mcvn2wrgx2-cpu/haxball-ui-framework
cd haxball-ui-framework
npm run build # β dist/ (ESM + CJS + IIFE)
npm run build:bundle # β haxball-ui.bundle.js (legacy IIFE)
npm run build:all # β bothHaxUI.init({ baseZ: 9000 }); // optional β auto-called on first createWindow()const win = HaxUI.createWindow({
id: 'my-window',
title: 'My Window',
width: 260,
height: 180,
x: 16,
y: 16,
content: '<p>Hello HaxBall</p>', // string or DOM Node
theme: 'default', // 'default' | 'haxball'
draggable: true, // drag from header
resizable: true, // resize from corners
titleVisible: true, // show/hide header on creation
closable: true // show close button (β)
});// Safe: pass a Node for external data (player names, chat β avoids XSS)
const node = document.createElement('div');
node.textContent = 'Goals: ' + data.goals;
win.setContent(node);
// Static markup
win.setContent('<p>Match ended</p>');win.show();
win.hide();
win.hideTitle(); // collapses the header
win.showTitle();win.destroy();
HaxUI.destroyWindow('my-window');
HaxUI.destroyAll(); // use on script unloadconst existing = HaxUI.getWindow('my-window'); // returns null if not found β never throws
if (existing) existing.setContent('<p>Updated</p>');HaxUI.createButton({
id: 'stats-btn',
label: 'π Stats',
onClick: () => win.show()
});
// Or open a window directly on click
HaxUI.createButton({
id: 'panel-btn',
label: 'π‘οΈ Admin',
onOpenWindow: { id: 'admin', title: 'Admin Panel', theme: 'haxball', width: 280, height: 400, x: 20, y: 60 }
});
HaxUI.destroyButton('stats-btn');HaxUI.diagnostics();
// β { initialized: true, mode: 'shadow', rootPresent: true, windowCount: 2, baseZ: 9000, version: 'v1' }theme: 'haxball' replicates HaxBall's native .dialog style using values
measured directly from the live DOM via getComputedStyle():
HaxUI.createWindow({
id: 'confirm',
title: 'Leave room?',
theme: 'haxball',
width: 300, height: 150, x: 100, y: 100,
content: '<p>Are you sure?</p><button>Leave</button>'
});| Property | Value |
|---|---|
| Background | rgb(26, 33, 37) |
| Border radius | 5px |
| Header accent | 3px solid rgb(193, 53, 53) |
| Font | "Open Sans", sans-serif |
| Button padding | 5px 15px |
| Button height | 26px |
| Button border | none |
| Decision | Risk mitigated |
|---|---|
| Shadow DOM per window (CSS namespace fallback) | HaxBall's global CSS bleeding into overlay elements |
Single window.HaxUI namespace |
Collisions with HaxBall's own globals or other scripts |
EventGuard per-event-type policy |
Keyboard/mouse events leaking into the game |
BASE_Z = 9000, configurable |
Overlay windows rendering behind HaxBall's own menus |
MutationObserver in RootMount |
HaxBall clearing the DOM on room transitions |
DOMParser in setContent() |
XSS when rendering external strings (player names, chat) |
WindowHandle._destroyed flag |
Safe post-destroy calls β no errors inside game callbacks |
| Drag/resize listeners in capture phase | EventGuard's bubble-phase stopPropagation was blocking drag release |
ButtonInjector polling for .header-btns |
Unreliable timing for a single MutationObserver |
Button styles measured via getComputedStyle() |
Visual mismatch with HaxBall's real native buttons |
import HaxUI from 'haxball-ui-framework';
const stats = HaxUI.createWindow({
id: 'stats', title: 'Stats', theme: 'haxball',
width: 260, height: 180, x: 16, y: 16
});
function onGameData(data) {
const node = document.createElement('div');
node.innerHTML = [
'<div>Team 1: ' + data.score[0] + '</div>',
'<div>Team 2: ' + data.score[1] + '</div>',
'<div>Time: ' + data.time + '</div>'
].join('');
stats.setContent(node);
}
function onLeaveRoom() { HaxUI.destroyAll(); }More examples in dev/examples.js.
- v0 β Core: windows, content updates, Shadow DOM, event isolation, DOM re-anchor
- v1 β Interaction: drag & drop, resize,
haxballtheme, native button injection,hideTitle, close button - v1.0.1 β npm:
import HaxUI from 'haxball-ui-framework', ESM + CJS + IIFE outputs - v2 β Components: component system for
setContent(), base components - v3 β Plugins:
HaxUI.use(plugin), window lifecycle hooks
v1.0.1 β stable. Published on npm. The v0/v1 API contract is frozen and won't break.
- minimal surface area, maximum control
- one global name, zero internals exposed
- every decision traceable to a real HaxBall environment risk
- extensible to v2βv3 without breaking existing code
MIT
β€οΈ DEVLOG