A theme is CSS variables, raw CSS, and optional raw JavaScript — reviewed by staff, then installable by any server with one click from the marketplace. This page covers what a theme can do and how to build one against your own real dispatch panel before submitting it.
What a theme can change
| Field | What it does |
|---|---|
| CSS variables | Custom properties merged onto the dispatch panel's root element — colors, spacing, fonts. The lowest-risk, easiest way to reskin. |
| Raw CSS | Additional rules layered on top, for anything a variable alone can't express. |
| Raw JavaScript | Runs with full DOM access inside the live panel. Can rebuild the entire panel from scratch, not just recolor it — see below. |
A CSS-only theme reskins the existing panel. JavaScript is what lets you go further — replace the panel's layout, add your own elements (a toast notification on a new 911 call, for example), or fully recreate the dispatch panel's DOM as your own design.
Building a theme against the real panel
Don't build blind against a mockup. Open your own dispatch panel, authenticated, and iterate against the real thing:
- CSS — use your browser's devtools (or any extension) to inject a
<style>tag directly into the live page. This is exactly how a theme's CSS is applied once installed, so what you see is what you get. - JavaScript — every dispatch panel exposes
window.__tjDev = { dispatch, theme }while you're logged in. These are the exact same objects your theme's JavaScript receives once installed (dispatchandtheme, passed as function arguments) — just reachable from your own console instead. There's no separate toggle or flag: being authenticated into the panel is already the only requirement.
// In devtools, on your own authenticated dispatch panel:
window.__tjDev.theme.hideDefaultUI();
window.__tjDev.theme.mountTemplate(`
<div data-rd-repeat="zone in zones" data-rd-key="zone.id">
<div data-rd-text="zone.name"></div>
<div data-rd-repeat="ch in zone.channels" data-rd-key="ch.id"
data-rd-class="active:ch.isCurrent" data-rd-on="click:join(ch.frequency)">
<span data-rd-text="ch.name"></span>
</div>
</div>
`);
// Real, live state:
window.__tjDev.dispatch.getViewModel();theme.mountTemplate(html) is the recommended way to rebuild the panel: hand it HTML annotated with data-rd-* attributes and it stays in sync with live data automatically — reconciled by key, so a single unit's status changing updates one node instead of re-rendering everything. No manual event wiring, no render loop. It's also safe by construction (no arbitrary code runs through it), which is why a theme that only uses mountTemplate and never touches raw jsText review is easier to get approved.
A practical tip for iterating on a template string: keep it in your browser's DevTools Snippets panel (Chrome: Sources → Snippets; Firefox has an equivalent) rather than retyping it into the console every time — a real multi-line editor that persists across reloads, with a keyboard shortcut to re-run. Editing the rendered DOM directly in the Elements panel doesn't work for anything mountTemplate controls — it reasserts state from the template on every live data change and will silently overwrite manual edits.
Once it looks right, copy your finished CSS and JavaScript into the theme editor and submit.
The theme editor: Simple vs. Advanced
- Simple — color/token controls that write into a visible CSS box (add or edit a variable there, or type CSS directly). Good for a straightforward reskin.
- Advanced — a raw CSS box and a raw JavaScript box, no controls in between. This is where you paste what you built against
window.__tjDevabove. Usetheme.mountTemplate()inside your JavaScript for a full custom panel — you don't need a separate "template" field, it's just JavaScript that callsmountTemplateonce.
Every theme is reviewed by staff before it goes live — JavaScript runs with full DOM access in a customer's real dispatch panel, so submissions containing JavaScript get read before approval. A CSS-only theme (no JavaScript) is lower-risk and reviewed accordingly.
Reference
- View model — what your template or JavaScript reads:
zones(each withchannels, each withusers),currentChannel,calls911,patches,alertTypes,transmissions,ptt,connection,settings, and more. Calldispatch.getViewModel()to see the full live shape. - Template attributes —
data-rd-text,data-rd-html,data-rd-show/data-rd-hide,data-rd-attr,data-rd-class,data-rd-style,data-rd-repeat+data-rd-key,data-rd-on. - Template actions (
data-rd-on) —join(freq),leave(),addScan(freq),removeScan(freq),startPtt(),stopPtt(),startChannelPtt(freq),stopChannelPtt(),activateGroup(id),acknowledgePanic(serverId, unitId?),openSettings(),closeSettings(). - Design tokens —
--bg-primary,--bg-secondary,--bg-hover,--text-primary,--text-secondary,--accent-raw,--danger,--success,--border,--radius,--my-dispatch-bg/--my-dispatch-border,--other-dispatch-bg/--other-dispatch-border,--font-sans,--font-size, and more.
This mirrors Tommy's Radio's own theming reference in more detail, including the full JS API (dispatch.on/off, channel/PTT/alert/patch methods, theme.setVar/addClass/getContainer) and security model.
