HUD Layouts
Build a custom tELS HUD overlay with HTML, CSS and optional JavaScript.
A layout is a self-contained HTML file plus assets in a folder under layouts/. It binds DOM
elements to state fields with data-hud-* attributes. Folders in layouts/ are
auto-discovered and listed in /tels → HUD Layout.
tELS ships CRX-600 and SPX-1800.
Create a layout
Create the folder
layouts/MyLayout/. The folder name is what players see in the selector.
Add ui.html
All HTML, CSS, and optionally one <script data-hud-script> block. Give the outermost
element a fixed size and declare it.
<div
class="ui-root"
style="position:relative;width:440px;height:214px;overflow:hidden"
data-radio-width="440"
data-radio-height="214"
data-hud-show="inVehicle"
>
…
</div>data-radio-width and data-radio-height are the fallback size while the layout is hidden.
Without them, a layout that starts hidden never becomes visible.
Add assets (optional)
layouts/MyLayout/
ui.html
icons/ PNG / SVG assets
sounds/ LIGHTS_ON.ogg, TONE_CHANGE.ogg, …
ui.html is served from inside layouts/<name>/, so relative paths resolve against it.
Restart the resource
The name appears without one, but ui.html and your assets 404 until you restart. A
marketplace layout needs no restart.
Layouts run isolated
Each layout loads as its own page, in a sandbox: its own document, its own window, no
access to the HUD around it or to the game.
| Boundary | |
|---|---|
| Scope | Everything inside your ui.html is yours alone, discarded when the player switches skins |
| State | Arrives from tELS. You do not fetch it |
| Output | Button presses leave as data-hud-btn actions |
| Reach | No route to the host page, to other resources, or to any native |
Data bindings
Processed on every update. Dot paths reach nested state: leds.R1 is state.leds.R1.
| Attribute | Example | Behaviour |
|---|---|---|
data-hud-text | data-hud-text="patternName" | Set textContent |
data-hud-html | data-hud-html="customHtml" | Set innerHTML. Careful with untrusted values |
data-hud-led | data-hud-led="sirenOn" | Toggle .active when truthy |
data-hud-show | data-hud-show="inVehicle" | Show when truthy |
data-hud-hide | data-hud-hide="engineOn" | Hide when truthy |
data-hud-eq | data-hud-eq="taMode:left" | Toggle .active when state[key] === value |
data-hud-attr | data-hud-attr="gear:data-gear" | Copy the value to an HTML attribute |
data-hud-led-color | data-hud-led-color="leds.R1" | Set background-color from {r, g, b, a} |
data-hud-btn | data-hud-btn="toggleSiren" | Click fires a Lua tels:hudButton callback with this action ID |
data-hud-keys | data-hud-keys="sirenTones,maxStage" | Declare extra data dependencies for scripts |
<div data-hud-show="inVehicle">
<span data-hud-text="patternName">—</span>
<div data-hud-led="sirenOn" class="indicator">SIREN</div>
<div data-hud-eq="taMode:left" class="arrow">◄</div>
<div data-hud-led-color="leds.R1" class="pip"></div>
<button data-hud-btn="cycle_pattern">Next</button>
</div>State keys
Only keys the layout references through data-hud-* or data-hud-keys are sent.
Core
| Key | Type | Description |
|---|---|---|
inVehicle | boolean | Player is in a vehicle |
active | boolean | Emergency lights on |
stage | number | 0 off, 1–3 |
maxStage | number | Max configured stages for this vehicle |
Pattern
| Key | Type | Description |
|---|---|---|
patternName | string | Current pattern. Empty for lights-only |
patternIdx | number | 1-based index. 0 is lights-only |
patternCount | number | Total patterns |
Siren
| Key | Type | Description |
|---|---|---|
sirenOn | boolean | Main siren playing |
sirenTone | number | Main tone index, 1-based |
sirenToneName | string | Main tone display name |
sirenTones | array | [{idx, name}, …] — every configured slot |
auxSirenOn | boolean | Aux siren playing |
auxSirenTone | number | Aux tone index |
auxToneName | string | Aux tone display name |
hornActive | boolean | Airhorn or horn held. Zero-latency, driven by a global flag |
chirping | boolean | Chirp tone playing |
rumblerOn | boolean | Rumbler variant active |
paActive | boolean | PA mic transmitting |
paAvailable | boolean | pma-voice running |
paRange | number | Current PA range, metres |
Modes
| Key | Type | Description |
|---|---|---|
cruise | boolean | Cruise on |
takedown | boolean | Takedown on |
taMode | string | "off", "left", "right", "center" |
presence | boolean | Presence detection armed |
Vehicle
| Key | Type | Description |
|---|---|---|
modelKey | string | Model key, lowercase |
modelName | string | Display name, e.g. "POLICE" |
speedMph | number | Rounded mph |
engineOn | boolean | Engine running |
gear | number | Current gear. 0 is reverse |
rpm | number | 0–100 |
health | number | Engine health, 0–1000 |
livery | number | Livery index |
locked | boolean | Vehicle locked |
isDriver | boolean | Player is in the driver seat |
Lights, controls and signals
| Key | Type | Description |
|---|---|---|
headlights / highbeams | boolean | On |
brake | boolean | Brake pedal pressed |
handbrake | boolean | Handbrake engaged |
reverse | boolean | Gear 0 and actually moving backward |
turnLeft / turnRight | boolean | That indicator on |
hazards | boolean | Hazards on |
Doors
| Key | Type | Description |
|---|---|---|
doorFL, doorFR, doorRL, doorRR | boolean | That door open |
hood / trunk | boolean | Open |
LEDs
| Key | Type | Description |
|---|---|---|
leds | object | Live LED colours keyed by LED name, e.g. { R1: { r, g, b, a } } |
Bind one with data-hud-led-color="leds.<name>". LED colours stream at roughly 20 Hz,
independently of the ~5 Hz full state update.
Layout scripts
For derived values or dynamic HTML, add a <script data-hud-script> block. tELS looks for
exactly the two function names below.
<script data-hud-script>
(function () {
window.__telsLayoutUpdate = function (state, root) {
var el = root.querySelector(".reverse-indicator");
if (el) el.style.display = state.reverse ? "" : "none";
var kphEl = root.querySelector(".speed-kph");
if (kphEl) kphEl.textContent = Math.round((state.speedMph || 0) * 1.609);
};
window.__telsLayoutDestroy = function () {
// cancel timers, detach listeners
};
})();
</script>Cache your query selectors in the closure — the callback runs every tick. If a script needs a
key no attribute binds, declare it, or Lua never computes it and your script gets
undefined:
<div data-hud-keys="sirenTones,maxStage,sirenTone">…</div>What a script can and cannot do
| You can | You cannot |
|---|---|
Read everything in state and paint it how you like | Reach the host page, or any other layout |
| Build, move and restyle elements inside your own layout | Call a game native or a NUI callback directly |
Use timers, requestAnimationFrame, canvas, WebGL, audio | Read or write player data, or anything outside your folder |
Ask the game to act, with a data-hud-btn | Open a window, navigate the page, or lock the pointer |
Load images and sounds from your folder, via markup or new Image() / new Audio() | Use fetch or XMLHttpRequest, even against your own folder |
| Use any font the player's system has | Load a custom web font with @font-face |
To use a specific typeface, embed it as a data: URL.
If a skin comes up empty, check the client console. A script that throws is reported there by layout name and reason, and the rest of the HUD keeps running.
Buttons
data-hud-btn="<action>" fires a tels:hudButton client event with the action string. Wire
it to your own handlers in a separate resource, or reuse the built-ins.
AddEventHandler("tels:hudButton", function(action)
if action == "cycle_pattern" then
ExecuteCommand("+lbar_pattern")
end
end)Sounds
tELS plays a file from your optional sounds/ folder on five actions, named verbatim for
the action: layouts/<your-layout>/sounds/<ACTION>.ogg.
| File | Plays on |
|---|---|
LIGHTS_ON.ogg | The lights coming on |
LIGHTS_OFF.ogg | The lights going off |
TONE_CHANGE.ogg | A siren tone change, including the start of a burst |
AIRHORN.ogg | The airhorn key |
Press.ogg | A pattern change and the PA key — the generic click |
A missing file plays nothing and is not an error, so ship only what you want.
UI Studio
Build and publish layouts with the UI Studio.
tELS
Still stuck? Support can answer it directly.
Was this page helpful?
TIMMYG Studios