Tutorial 2026-03-19

Custom HUD Development for FiveM - From Scratch

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

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.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.