Skip to content

Build your first custom widget

  • 2 files widget.json and index.html
  • ~1.5 s until the overlay picks the folder up
  • 0 build steps, no TypeScript

The custom widget path is the fastest way onto the overlay: a folder with an index.html and a widget.json becomes a live, hot-reloaded hub — the panel chrome, drag, hover-reveal, and auto-collapse are drawn for you. No build step, no TypeScript. It’s locked down by default — it gets a limited set of capabilities declared in its widget.json (and can run a restricted Node.js backend), but it can’t extend TeamHUD’s internals or reach native code. For full trust, build a plugin.

This is also the path the in-app AI chat uses when you ask it to build a widget.

  • The TeamHUD app running.
  • Nothing else — you edit plain HTML/CSS/JS.

Custom widgets live in a watched folder. Your personal one survives app updates:

~/.teamhud/custom-widgets/my-widget/
widget.json
index.html

The overlay picks up the folder within ~1.5 s — no restart. If it doesn’t appear, enable it from the widget library.

widget.json is the only required config beyond your HTML:

{
"name": "My Widget", // hub label
"html": "index.html", // your UI (local file or an https:// URL)
"icon": "icon.svg", // optional hub icon — SVG only
"size": { "width": 300, "height": 220 },
"permissions": { "backend": false, "node": [], "network": false }
}
"permissions": {
"backend": false, "node": [], "network": true,
"networkHosts": [{ "host": "api.example.com", "methods": ["GET", "POST"] }] // or "*.example.com"
}

Omitting methods means GET only: a widget that reads fine and then fails on its first POST has declared the host but not the method.

The widget’s iframe is fenced to exactly this list. The person approves network once when the widget is installed (Preferences → Plugins) and can switch it off later under Preferences → Privacy & Security; the hosts are yours to declare, and they can only grant what you asked for. Every permission is listed in Widget permissions.

Two rules make your widget match the HUD by construction:

  • Use --th-* tokens for color, type, spacing, and radius — never hardcode hex, px font sizes, or font-family literals. The host injects a live snapshot of every token and re-pushes them when the user changes density, accent, or theme, so var(--th-…) tracks the HUD automatically.
  • Mark clickable areas class="teamhud-interactive" so the click-through overlay lets pointer events reach them.

A starter index.html:

<!doctype html>
<body style="margin: 0; font-family: var(--th-font-ui); color: var(--th-text-primary);">
<div id="app" class="teamhud-interactive" style="padding: 12px; display: flex; flex-direction: column; gap: 8px;">
<div style="font-size: var(--th-text-sm); color: var(--th-text-muted);">Clicks</div>
<div id="count" style="font-size: var(--th-text-xl);">0</div>
<button id="bump" style="font: inherit; padding: 6px 10px; border-radius: var(--th-radius-md);
border: 1px solid var(--th-border-secondary); background: var(--th-bg-surface); color: var(--th-text-primary);">
Count up
</button>
</div>
<script>
let n = 0;
const out = document.getElementById('count');
document.getElementById('bump').addEventListener('click', () => { out.textContent = String(++n); });
</script>
</body>

Save, and the overlay hot-reloads. Common tokens: --th-text-primary / --th-text-muted (text), --th-text-sm/-md/-lg/-xl (sizes), --th-bg-surface / --th-bg-hover (surfaces), --th-border-secondary (borders), --th-radius-md (corners), --th-space-1…--th-space-6 (spacing), --th-accent (accent). The full catalog, the shared .th-* classes/elements, and the rules are in Theming & styling.

A locked-down overlay widget that themes itself to the HUD automatically. When you need to extend TeamHUD’s internals or run with full trust, cross over to building a plugin.

Let the AI do it

Ask the in-app AI chat to build a widget — it authors this same folder shape behind a confirmation card.