HUD Layouts
Build custom HUD overlays for tELS with HTML, CSS, and optional JavaScript.
Overview
A HUD layout is a self-contained HTML file plus assets in a folder under layouts/. Lua pushes vehicle state to the NUI every tick, and the layout binds DOM elements to state fields with data-hud-* attributes. Lua does no presentation logic — the layout decides how to display what it receives.
Folders in layouts/ are auto-discovered by the server and appear in the HUD Layout selector in /tels.
Create a layout
Create the folder
layouts/MyLayout/ — the folder name is what players see in the layout selector.
Add ui.html
This is the layout's entry point. It can contain all HTML, CSS, and (optionally) a single <script data-hud-script> block for presentation logic.
Add assets (optional)
layouts/MyLayout/
ui.html
icons/ PNG / SVG assets
sounds/ On.ogg, Off.ogg, Press.ogg (optional)Reference assets relatively. ui.html is served from inside layouts/<name>/ — use paths like icons/foo.png.
Restart the resource
Layouts are enumerated on server start. Restart tELS and the new layout appears in /tels → HUD Layout.
Data bindings
Attach data-hud-* attributes to DOM elements. The binder processes them on every update.
| Attribute | Example | Behavior |
|---|---|---|
data-hud-text | data-hud-text="patternName" | Set textContent to the state value |
data-hud-html | data-hud-html="customHtml" | Set innerHTML (be careful with untrusted values) |
data-hud-led | data-hud-led="sirenOn" | Toggle .active class when truthy |
data-hud-show | data-hud-show="inVehicle" | Show when truthy, hide when falsy |
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 state 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 (see below) |
Use dot paths for nested state: leds.R1 resolves to state.leds.R1.
Example
<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>Available state keys
Lua sends raw data only — no derived strings or presentation. Every key below is optional: the binder only sends keys the layout references (via data-hud-* attributes or data-hud-keys).
Core
| Key | Type | Description |
|---|---|---|
inVehicle | boolean | Player is in a vehicle |
active | boolean | Emergency lights are on |
stage | number | Current stage (0 = off, 1–3) |
maxStage | number | Max configured stages for this vehicle |
Pattern
| Key | Type | Description |
|---|---|---|
patternName | string | Current pattern name (empty for lights-only) |
patternIdx | number | 1-based pattern index (0 = lights-only) |
patternCount | number | Total patterns for this vehicle |
Siren
| Key | Type | Description |
|---|---|---|
sirenOn | boolean | Main siren playing |
sirenTone | number | Main siren tone index (1-based) |
sirenToneName | string | Main siren tone display name |
sirenTones | array | [{idx, name}, ...] — all configured tone slots |
auxSirenOn | boolean | Aux siren playing |
auxSirenTone | number | Aux siren tone index |
auxToneName | string | Aux siren tone display name |
hornActive | boolean | Airhorn or horn held (zero-latency, driven by global flag) |
chirping | boolean | Chirp tone currently playing |
rumblerOn | boolean | Rumbler variant active |
Modes
| Key | Type | Description |
|---|---|---|
cruise | boolean | Cruise on |
takedown | boolean | Takedown on |
taMode | string | "off", "left", "right", or "center" |
presence | boolean | Presence detection armed |
Vehicle
| Key | Type | Description |
|---|---|---|
modelKey | string | Vehicle model key (lowercase) |
modelName | string | Display name (e.g. "POLICE") |
speedMph | number | Rounded speed in mph |
engineOn | boolean | Engine running |
gear | number | Current gear (0 = reverse) |
rpm | number | 0–100 scaled RPM |
health | number | Engine health (0–1000) |
livery | number | Livery index |
locked | boolean | Vehicle locked |
isDriver | boolean | Player is in the driver seat |
Lights
| Key | Type | Description |
|---|---|---|
headlights | boolean | Headlights on |
highbeams | boolean | High beams on |
Controls
| Key | Type | Description |
|---|---|---|
brake | boolean | Brake pedal pressed |
handbrake | boolean | Handbrake engaged |
reverse | boolean | Gear 0 + actually moving backward |
Signals
| Key | Type | Description |
|---|---|---|
turnLeft | boolean | Left indicator on |
turnRight | boolean | Right indicator on |
hazards | boolean | Hazards on |
Doors
| Key | Type | Description |
|---|---|---|
doorFL, doorFR, doorRL, doorRR | boolean | That door is open |
hood | boolean | Hood open |
trunk | boolean | Trunk open |
LEDs
| Key | Type | Description |
|---|---|---|
leds | object | Live LED colors, keyed by LED name (e.g., { R1: { r, g, b, a } }) |
Bind individual LEDs with data-hud-led-color="leds.<name>". The LED colors stream at roughly 20 Hz, independent of the full state update.
Layout scripts
For derived values, dynamic HTML, or anything beyond simple bindings, add a <script data-hud-script> block. The script registers a callback that runs after each binder pass.
<script data-hud-script>
(function () {
window.__telsLayoutUpdate = function (state, root) {
// state: raw data from Lua
// root: this layout's container element
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);
};
// Optional cleanup when the layout is unloaded.
window.__telsLayoutDestroy = function () {
// cancel timers, detach listeners, etc.
};
})();
</script>Declaring extra data
Lua only sends keys the layout references in data-hud-* attributes. If a script needs a key that isn't bound to any attribute, declare it with data-hud-keys:
<div data-hud-keys="sirenTones,maxStage,sirenTone">
…
</div>Without this, Lua skips computing those keys and your script receives undefined.
Buttons
data-hud-btn="<action>" fires a tels:hudButton client event with the action string:
AddEventHandler("tels:hudButton", function(action)
if action == "cycle_pattern" then
ExecuteCommand("+lbar_pattern")
end
end)Wire it to your own handlers in a separate resource, or reuse the built-in ones.
Sounds
Optional sounds/ folder. The HUD plays:
On.ogg— on toggle-on actionsOff.ogg— on toggle-off actionsPress.ogg— on button press
File paths resolve relative to the layout folder: layouts/<name>/sounds/<file>.ogg.
Browser preview
You can preview layouts in a regular browser without launching FiveM by including the devtools script:
<script src="../devtools.js"></script>It injects a floating control panel with toggles and sliders for every state key. The script detects the NUI environment and is a no-op when running in-game.
Build devtools from the resource root: bun run build.
Performance notes
- Lua only computes keys the active layout references.
data-hud-*attributes anddata-hud-keystogether drive this filter. - Full state updates run at ~5 Hz. LED colors (
leds.*) update separately at ~20 Hz. - Cache query selectors in your script closure — it runs on every tick.
