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;
}