FixFX

NUI Development

Complete guide to developing HTML-based user interfaces for FiveM resources.

NUI (New UI) allows you to create rich, interactive web-based interfaces for your FiveM resources using HTML, CSS, and JavaScript. This system provides seamless integration between your web UI and the game.

NUI Basics

Setting up NUI

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'
 
ui_page 'html/index.html'
 
files {
    'html/index.html',
    'html/style.css',
    'html/script.js',
    'html/assets/*'
}
 
client_script 'client.lua'

Basic HTML Structure

html/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Resource UI</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app" class="hidden">
        <div class="container">
            <h1>Welcome to My Resource</h1>
            <button id="closeBtn">Close</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

html/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
body {
    font-family: 'Arial', sans-serif;
    background: transparent;
    overflow: hidden;
}
 
.hidden {
    display: none !important;
}
 
#app {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
}
 
.container {
    background: #2a2a2a;
    border-radius: 10px;
    padding: 20px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
    color: white;
    text-align: center;
}
 
button {
    background: #007bff;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 10px;
}
 
button:hover {
    background: #0056b3;
}

Client-Side Integration

Opening and Closing NUI

client.lua

local isUIOpen = false
 
function OpenUI()
    if isUIOpen then return end
    
    isUIOpen = true
    SetNuiFocus(true, true)
    
    SendNUIMessage({
        action = "show"
    })
end
 
function CloseUI()
    if not isUIOpen then return end
    
    isUIOpen = false
    SetNuiFocus(false, false)
    
    SendNUIMessage({
        action = "hide"
    })
end
 
-- Register command to open UI
RegisterCommand('openui', function()
    OpenUI()
end)
 
-- Handle NUI callbacks
RegisterNUICallback('close', function(data, cb)
    CloseUI()
    cb('ok')
end)
 
-- Close UI with ESC key
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        
        if isUIOpen and IsControlJustPressed(0, 322) then -- ESC key
            CloseUI()
        end
    end
end)

JavaScript Communication

html/script.js

const app = document.getElementById('app');
const closeBtn = document.getElementById('closeBtn');
 
// Listen for messages from the game
window.addEventListener('message', function(event) {
    const data = event.data;
    
    switch(data.action) {
        case 'show':
            showUI(data.data);
            break;
        case 'hide':
            hideUI();
            break;
        case 'update':
            updateUI(data.data);
            break;
    }
});
 
function showUI(data = {}) {
    app.classList.remove('hidden');
    document.body.style.display = 'block';
    
    // Update UI with provided data
    if (data) {
        updateUI(data);
    }
}
 
function hideUI() {
    app.classList.add('hidden');
    document.body.style.display = 'none';
}
 
function updateUI(data) {
    // Update UI elements with new data
    console.log('Updating UI with:', data);
}
 
// Close button event
closeBtn.addEventListener('click', function() {
    fetch(`https://${GetParentResourceName()}/close`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({})
    });
});
 
// Helper function to get resource name
function GetParentResourceName() {
    return window.location.hostname;
}
 
// Send data back to the game
function sendToGame(action, data = {}) {
    fetch(`https://${GetParentResourceName()}/${action}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
    });
}

Advanced NUI Features

Data Binding and Updates

client.lua

local playerData = {
    name = '',
    money = 0,
    job = '',
    level = 1
}
 
function UpdatePlayerData(newData)
    for k, v in pairs(newData) do
        playerData[k] = v
    end
    
    -- Send updated data to NUI
    SendNUIMessage({
        action = "updatePlayerData",
        data = playerData
    })
end
 
-- Example: Update money when it changes
RegisterNetEvent('money:updated')
AddEventHandler('money:updated', function(newAmount)
    UpdatePlayerData({money = newAmount})
end)

html/script.js

let playerData = {};
 
window.addEventListener('message', function(event) {
    const data = event.data;
    
    switch(data.action) {
        case 'updatePlayerData':
            playerData = data.data;
            updatePlayerDataDisplay();
            break;
    }
});
 
function updatePlayerDataDisplay() {
    document.getElementById('playerName').textContent = playerData.name;
    document.getElementById('playerMoney').textContent = `$${playerData.money.toLocaleString()}`;
    document.getElementById('playerJob').textContent = playerData.job;
    document.getElementById('playerLevel').textContent = playerData.level;
}

Form Handling

html/index.html

<form id="transferForm">
    <input type="number" id="amount" placeholder="Amount" required>
    <input type="number" id="targetId" placeholder="Target Player ID" required>
    <input type="text" id="reason" placeholder="Reason (optional)">
    <button type="submit">Transfer Money</button>
</form>

html/script.js

document.getElementById('transferForm').addEventListener('submit', function(e) {
    e.preventDefault();
    
    const formData = {
        amount: parseInt(document.getElementById('amount').value),
        targetId: parseInt(document.getElementById('targetId').value),
        reason: document.getElementById('reason').value
    };
    
    // Validate form data
    if (formData.amount <= 0) {
        showError('Amount must be greater than 0');
        return;
    }
    
    if (formData.targetId <= 0) {
        showError('Invalid target player ID');
        return;
    }
    
    // Send to game
    sendToGame('transferMoney', formData);
});
 
function showError(message) {
    // Display error message to user
    const errorDiv = document.getElementById('errorMessage');
    errorDiv.textContent = message;
    errorDiv.style.display = 'block';
    
    setTimeout(() => {
        errorDiv.style.display = 'none';
    }, 3000);
}

client.lua

RegisterNUICallback('transferMoney', function(data, cb)
    -- Validate data on client side too
    if not data.amount or not data.targetId then
        cb({success = false, error = 'Missing required fields'})
        return
    end
    
    -- Send to server for processing
    TriggerServerEvent('banking:transferMoney', data)
    
    cb({success = true})
end)

Real-time Updates

client.lua

-- Update UI every second with current time
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(1000)
        
        if isUIOpen then
            SendNUIMessage({
                action = "updateTime",
                data = {
                    time = os.date('%H:%M:%S'),
                    date = os.date('%Y-%m-%d')
                }
            })
        end
    end
end)
 
-- Send notifications to UI
RegisterNetEvent('ui:notify')
AddEventHandler('ui:notify', function(message, type)
    SendNUIMessage({
        action = "showNotification",
        data = {
            message = message,
            type = type or 'info'
        }
    })
end)

html/script.js

window.addEventListener('message', function(event) {
    const data = event.data;
    
    switch(data.action) {
        case 'updateTime':
            updateTimeDisplay(data.data);
            break;
        case 'showNotification':
            showNotification(data.data.message, data.data.type);
            break;
    }
});
 
function updateTimeDisplay(timeData) {
    document.getElementById('currentTime').textContent = timeData.time;
    document.getElementById('currentDate').textContent = timeData.date;
}
 
function showNotification(message, type) {
    const notification = document.createElement('div');
    notification.className = `notification ${type}`;
    notification.textContent = message;
    
    document.body.appendChild(notification);
    
    // Auto-remove after 3 seconds
    setTimeout(() => {
        notification.remove();
    }, 3000);
}

Modern JavaScript Frameworks

Using Vue.js

html/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue NUI App</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app" v-show="visible">
        <div class="container">
            <h1>{{ title }}</h1>
            <p>Money: ${{ playerData.money }}</p>
            <p>Job: {{ playerData.job }}</p>
            
            <form @submit.prevent="submitForm">
                <input v-model="formData.amount" type="number" placeholder="Amount">
                <button type="submit">Submit</button>
            </form>
            
            <button @click="closeUI">Close</button>
        </div>
    </div>
 
    <script>
        const { createApp } = Vue;
 
        createApp({
            data() {
                return {
                    visible: false,
                    title: 'Banking System',
                    playerData: {
                        money: 0,
                        job: 'unemployed'
                    },
                    formData: {
                        amount: 0
                    }
                }
            },
            methods: {
                closeUI() {
                    this.visible = false;
                    fetch(`https://${GetParentResourceName()}/close`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({})
                    });
                },
                submitForm() {
                    fetch(`https://${GetParentResourceName()}/submitForm`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(this.formData)
                    });
                },
                handleMessage(event) {
                    const data = event.data;
                    
                    switch(data.action) {
                        case 'show':
                            this.visible = true;
                            if (data.data) {
                                this.playerData = { ...this.playerData, ...data.data };
                            }
                            break;
                        case 'hide':
                            this.visible = false;
                            break;
                        case 'updateData':
                            this.playerData = { ...this.playerData, ...data.data };
                            break;
                    }
                }
            },
            mounted() {
                window.addEventListener('message', this.handleMessage);
            },
            beforeUnmount() {
                window.removeEventListener('message', this.handleMessage);
            }
        }).mount('#app');
 
        function GetParentResourceName() {
            return window.location.hostname;
        }
    </script>
</body>
</html>

Using React (with CDN)

html/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React NUI App</title>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="root"></div>
 
    <script type="text/babel">
        const { useState, useEffect } = React;
 
        function App() {
            const [visible, setVisible] = useState(false);
            const [playerData, setPlayerData] = useState({
                money: 0,
                job: 'unemployed'
            });
 
            useEffect(() => {
                const handleMessage = (event) => {
                    const data = event.data;
                    
                    switch(data.action) {
                        case 'show':
                            setVisible(true);
                            if (data.data) {
                                setPlayerData(prev => ({ ...prev, ...data.data }));
                            }
                            break;
                        case 'hide':
                            setVisible(false);
                            break;
                        case 'updateData':
                            setPlayerData(prev => ({ ...prev, ...data.data }));
                            break;
                    }
                };
 
                window.addEventListener('message', handleMessage);
                return () => window.removeEventListener('message', handleMessage);
            }, []);
 
            const closeUI = () => {
                setVisible(false);
                fetch(`https://${window.location.hostname}/close`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({})
                });
            };
 
            if (!visible) return null;
 
            return (
                <div className="app">
                    <div className="container">
                        <h1>Banking System</h1>
                        <p>Money: ${playerData.money}</p>
                        <p>Job: {playerData.job}</p>
                        <button onClick={closeUI}>Close</button>
                    </div>
                </div>
            );
        }
 
        ReactDOM.render(<App />, document.getElementById('root'));
    </script>
</body>
</html>

CSS Animations and Transitions

Smooth Animations

html/style.css

.app {
    opacity: 0;
    transform: scale(0.8);
    transition: all 0.3s ease-in-out;
}
 
.app.visible {
    opacity: 1;
    transform: scale(1);
}
 
.container {
    transform: translateY(-20px);
    transition: transform 0.3s ease-out;
}
 
.app.visible .container {
    transform: translateY(0);
}
 
/* Notification animations */
.notification {
    position: fixed;
    top: 20px;
    right: 20px;
    background: #333;
    color: white;
    padding: 15px;
    border-radius: 5px;
    transform: translateX(100%);
    transition: transform 0.3s ease-out;
}
 
.notification.show {
    transform: translateX(0);
}
 
.notification.success {
    background: #28a745;
}
 
.notification.error {
    background: #dc3545;
}
 
.notification.warning {
    background: #ffc107;
    color: #333;
}

Loading States

html/script.js

function showLoading() {
    const loading = document.createElement('div');
    loading.id = 'loading';
    loading.innerHTML = `
        <div class="spinner"></div>
        <p>Loading...</p>
    `;
    document.body.appendChild(loading);
}
 
function hideLoading() {
    const loading = document.getElementById('loading');
    if (loading) {
        loading.remove();
    }
}

html/style.css

#loading {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: white;
}
 
.spinner {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
}
 
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

Best Practices

Performance Optimization

// Debounce function for search inputs
function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}
 
// Usage
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
    sendToGame('search', { query });
}, 300);
 
searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

Error Handling

// Wrapper for fetch requests
async function safePost(endpoint, data) {
    try {
        const response = await fetch(`https://${GetParentResourceName()}/${endpoint}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        return await response.json();
    } catch (error) {
        console.error('Request failed:', error);
        showNotification('Request failed. Please try again.', 'error');
        return null;
    }
}

Responsive Design

/* Mobile-first responsive design */
.container {
    width: 90%;
    max-width: 400px;
    padding: 20px;
}
 
@media (min-width: 768px) {
    .container {
        max-width: 600px;
        padding: 30px;
    }
}
 
@media (min-width: 1024px) {
    .container {
        max-width: 800px;
        padding: 40px;
    }
}
 
/* Scale UI based on game resolution */
html {
    font-size: calc(12px + 0.5vw);
}

Security Considerations

// Sanitize user input
function sanitizeInput(input) {
    const div = document.createElement('div');
    div.textContent = input;
    return div.innerHTML;
}
 
// Validate data before sending
function validateTransferData(data) {
    if (typeof data.amount !== 'number' || data.amount <= 0) {
        return false;
    }
    
    if (typeof data.targetId !== 'number' || data.targetId <= 0) {
        return false;
    }
    
    if (data.reason && typeof data.reason !== 'string') {
        return false;
    }
    
    return true;
}