Why Build a Custom HUD
The default GTA V HUD was designed for a single-player action game, not for the complex roleplay scenarios that FiveM servers demand. A custom HUD lets you display roleplay-specific information like hunger, thirst, stress, and job status that simply do not exist in vanilla GTA. Beyond functionality, a custom HUD defines your server's visual identity and sets the tone from the moment a player spawns in. Players immediately notice the quality difference between a server running default UI elements and one with a polished, purpose-built HUD that matches the server's branding and theme. Building your own HUD from scratch also means you control every aspect of performance, ensuring the overlay runs at minimal resource cost while providing exactly the information your players need.
NUI Resource Structure
FiveM uses NUI (New User Interface) to render HTML, CSS, and JavaScript inside the game client. Your HUD resource needs a specific folder structure to work correctly. The fxmanifest.lua file declares the resource metadata, the client.lua script sends game data to the NUI layer, and the html/ folder contains your web-based interface. Here is a minimal resource manifest to get started:
fx_version 'cerulean'
game 'gta5'
description 'Custom HUD'
author 'YourName'
version '1.0.0'
ui_page 'html/index.html'
client_script 'client.lua'
files {
'html/index.html',
'html/style.css',
'html/script.js',
'html/fonts/*.woff2'
}
The ui_page directive tells FiveM which HTML file to render as the NUI overlay, and the files table lists every asset the browser needs access to. If you forget to include a CSS file or font in this list, the browser silently fails to load it without any error message, which is a common source of confusion for developers new to NUI development. Keep your resource structure clean by separating concerns: HTML for layout, CSS for styling, and JavaScript for logic and animations.
Sending Game Data to the UI
The client-side Lua script is responsible for reading game state and pushing it to the NUI layer at regular intervals. You need to gather health, armor, and any framework-specific status values like hunger and thirst. The key function is SendNUIMessage(), which sends a JSON payload to the JavaScript running in the NUI frame. Be strategic about your update frequency because sending data every frame wastes CPU cycles while updating too slowly makes the HUD feel laggy. A tick rate of 200-500 milliseconds strikes the right balance for status bars:
CreateThread(function()
while true do
local ped = PlayerPedId()
local health = GetEntityHealth(ped) - 100 -- GTA health starts at 100
local maxHealth = GetEntityMaxHealth(ped) - 100
local armor = GetPedArmour(ped)
-- Framework-specific data (QBCore example)
local playerData = QBCore.Functions.GetPlayerData()
local hunger = playerData.metadata['hunger'] or 100
local thirst = playerData.metadata['thirst'] or 100
local stress = playerData.metadata['stress'] or 0
SendNUIMessage({
action = 'updateStatus',
health = math.floor((health / maxHealth) * 100),
armor = armor,
hunger = math.floor(hunger),
thirst = math.floor(thirst),
stress = math.floor(stress)
})
Wait(300)
end
end)
For the speedometer, you need a separate thread running at a faster tick rate because speed changes rapidly and players expect real-time feedback when driving. A 50-100 millisecond interval works well for vehicle data. Only run the speedometer thread when the player is actually in a vehicle to save resources, and disable it when they exit:
CreateThread(function()
while true do
local ped = PlayerPedId()
if IsPedInAnyVehicle(ped, false) then
local veh = GetVehiclePedIsIn(ped, false)
local speed = GetEntitySpeed(veh) * 3.6 -- Convert to km/h
local rpm = GetVehicleCurrentRpm(veh)
local gear = GetVehicleCurrentGear(veh)
local fuel = GetVehicleFuelLevel(veh)
SendNUIMessage({
action = 'updateVehicle',
speed = math.floor(speed),
rpm = rpm,
gear = gear,
fuel = math.floor(fuel)
})
Wait(50)
else
SendNUIMessage({ action = 'hideVehicle' })
Wait(500)
end
end
end)
Building the HTML and CSS Interface
The visual design of your HUD determines how it feels in-game. Modern FiveM HUDs use clean, minimal designs with semi-transparent backgrounds, rounded corners, and subtle animations. Position your status bars in the bottom-left corner where they do not obstruct gameplay, and place the speedometer in the bottom-right when the player is driving. Use CSS custom properties for colors so you can easily theme the entire HUD by changing a few variables. Animated progress bars with smooth transitions give the HUD a polished feel:
<!-- html/index.html -->
<div id="hud-container">
<div class="status-bars">
<div class="bar-wrapper">
<i class="icon health-icon"></i>
<div class="bar">
<div class="bar-fill health-fill" id="health-bar"></div>
</div>
</div>
<div class="bar-wrapper">
<i class="icon armor-icon"></i>
<div class="bar">
<div class="bar-fill armor-fill" id="armor-bar"></div>
</div>
</div>
<div class="bar-wrapper">
<i class="icon hunger-icon"></i>
<div class="bar">
<div class="bar-fill hunger-fill" id="hunger-bar"></div>
</div>
</div>
<div class="bar-wrapper">
<i class="icon thirst-icon"></i>
<div class="bar">
<div class="bar-fill thirst-fill" id="thirst-bar"></div>
</div>
</div>
</div>
<div class="speedometer" id="speedometer" style="display:none">
<div class="speed-value" id="speed-value">0</div>
<div class="speed-unit">KM/H</div>
<div class="fuel-bar">
<div class="fuel-fill" id="fuel-bar"></div>
</div>
</div>
</div>
For the CSS, use transition on the bar width to create smooth fill animations, and apply pointer-events: none to the entire HUD container so it does not interfere with game input. Different colors for each status bar help players quickly identify which resource is low without reading labels. Red for health, blue for armor, orange for hunger, and cyan for thirst is a widely adopted convention that players intuitively understand.
JavaScript Message Handling
The JavaScript layer receives messages from the Lua client and updates the DOM accordingly. Register a message event listener that dispatches based on the action field in the payload. Keep your update logic lightweight because it runs at whatever frequency your Lua threads send data, and any lag in the JavaScript layer translates directly to visual stuttering. Avoid DOM queries inside the update handler by caching element references at initialization:
// html/script.js
const elements = {
healthBar: document.getElementById('health-bar'),
armorBar: document.getElementById('armor-bar'),
hungerBar: document.getElementById('hunger-bar'),
thirstBar: document.getElementById('thirst-bar'),
speedometer: document.getElementById('speedometer'),
speedValue: document.getElementById('speed-value'),
fuelBar: document.getElementById('fuel-bar')
};
window.addEventListener('message', (event) => {
const data = event.data;
switch (data.action) {
case 'updateStatus':
elements.healthBar.style.width = data.health + '%';
elements.armorBar.style.width = data.armor + '%';
elements.hungerBar.style.width = data.hunger + '%';
elements.thirstBar.style.width = data.thirst + '%';
// Color shift when low
if (data.health < 25) {
elements.healthBar.classList.add('critical');
} else {
elements.healthBar.classList.remove('critical');
}
break;
case 'updateVehicle':
elements.speedometer.style.display = 'flex';
elements.speedValue.textContent = data.speed;
elements.fuelBar.style.width = data.fuel + '%';
break;
case 'hideVehicle':
elements.speedometer.style.display = 'none';
break;
}
});
Add a CSS class for critical states that triggers a pulsing animation on the bar, drawing the player's attention when their health or hunger drops dangerously low. This kind of visual feedback is far more effective than relying on players to constantly monitor their status numbers, and it adds a layer of polish that separates amateur HUDs from professional ones.
Minimap Customization
The default GTA minimap is functional but visually clashes with most custom HUD designs. FiveM gives you control over the minimap's position, size, shape, and zoom level through native functions. You can create a circular minimap, move it to match your HUD layout, or even hide it entirely and replace it with a custom solution. The most common approach is reshaping the minimap to complement your HUD's aesthetic while keeping the underlying game map functionality intact:
CreateThread(function()
-- Wait for map to load
Wait(500)
-- Set minimap shape and position
local minimapHandle = RequestScaleformMovie('MINIMAP')
SetMinimapClipType(1) -- 0 = rectangle, 1 = circle
-- Adjust minimap position and size
local defaultAspect = 1920 / 1080
local resX, resY = GetActiveScreenResolution()
local aspect = resX / resY
local ratio = defaultAspect / aspect
SetMinimapComponentPosition('minimap', 'L', 'B',
0.0, -0.032, 0.145 * ratio, 0.210)
SetMinimapComponentPosition('minimap_mask', 'L', 'B',
0.0, 0.032, 0.128 * ratio, 0.300)
SetMinimapComponentPosition('minimap_blur', 'L', 'B',
-0.01, -0.032, 0.272 * ratio, 0.420)
-- Hide default health and armor bars
local minimap = RequestScaleformMovie('MINIMAP')
while not HasScaleformMovieLoaded(minimap) do Wait(0) end
while true do
-- Disable default HUD components
HideHudComponentThisFrame(6) -- Vehicle name
HideHudComponentThisFrame(7) -- Area name
HideHudComponentThisFrame(8) -- Vehicle class
HideHudComponentThisFrame(9) -- Street name
Wait(0)
end
end)
When customizing the minimap, always account for different screen aspect ratios. A minimap that looks perfect on a 16:9 display will be stretched or mispositioned on ultrawide monitors. Calculate the ratio between the default and actual aspect ratio and apply it to the width components. Test your minimap on at least three common resolutions (1920x1080, 2560x1440, and 3440x1440) to ensure consistent positioning across different player setups.
Hiding Default GTA HUD Elements
When you build a custom HUD, you must hide the default GTA elements that your custom UI replaces, otherwise players see duplicate information. FiveM provides the HideHudComponentThisFrame() native for this purpose, but it must be called every frame because GTA re-enables HUD components each tick. The component IDs cover everything from the wanted stars to the cash display to the weapon wheel. For a full replacement HUD, you typically want to hide the health bar, armor bar, cash display, and vehicle indicators while keeping essential elements like subtitles and notification popups visible. Create a configurable table of component IDs so server owners can toggle which default elements to hide without modifying your code. Additionally, use DisplayRadar(false) if your HUD includes its own minimap replacement, but be aware that hiding the radar also disables the pause menu map unless you re-enable it when the pause menu is detected.
Performance Best Practices
HUD performance is critical because your HUD runs constantly while the player is in-game, making even small inefficiencies compound into noticeable frame drops over extended play sessions. The single most impactful optimization is controlling your update frequency. Status bars that change slowly like hunger and thirst can update every 500 milliseconds or even every second, while fast-changing values like speed need more frequent updates. Use conditional rendering to only send NUI messages when values actually change, rather than pushing the same data repeatedly. On the JavaScript side, avoid innerHTML for updates because it forces the browser to reparse HTML, and use textContent or direct style property changes instead. Minimize CSS animations on elements that update frequently because the browser's animation engine and your JavaScript updates can conflict, causing visual glitches. If your HUD uses custom fonts, preload them in your HTML head to prevent layout shifts when the font file finishes loading. Finally, profile your resource using FiveM's built-in resmon command and aim for your HUD to consume less than 0.1ms per frame on average, leaving headroom for the dozens of other resources running on the client.