Building a custom notification UI for FiveM
Build a small resource with a client export (exports.mynotify:notify({ title, text, type, duration })) and a matching net event for the server, which sends a NUI message to a stack of toasts. Limit how many show at once, deduplicate repeats, place the stack away from the minimap and chat, and route framework notifications (ESX, QBCore, ox_lib) through it so everything looks the same.
Notifications are how every script talks to players — and on most servers they come in three different styles because ESX, QBCore, ox_lib and each job script ship their own. A single notification resource with a clean API gives the whole server one voice.
The Lua API
local function notify(data)
if type(data) == 'string' then data = { text = data } end
SendNUIMessage({
action = 'notify',
title = data.title,
text = data.text or '',
type = data.type or 'info',
duration = data.duration or 5000,
})
end
exports('notify', notify)
RegisterNetEvent('mynotify:notify', notify)-- client
exports.mynotify:notify({ title = 'Garage', text = 'Vehicle stored', type = 'success' })
-- server
TriggerClientEvent('mynotify:notify', src, { text = 'Payment received', type = 'success' })The notification stack
const stack = document.querySelector('#notify');
const MAX = 4;
window.addEventListener('message', ({ data }) => {
if (data.action !== 'notify') return;
const same = [...stack.children].find((n) => n.dataset.key === data.type + data.text);
if (same) {
const count = Number(same.dataset.count || 1) + 1;
same.dataset.count = count;
same.querySelector('.count').textContent = '×' + count;
return;
}
const el = document.createElement('div');
el.className = 'toast ' + data.type;
el.dataset.key = data.type + data.text;
el.innerHTML = '<strong></strong><p></p><span class="count"></span>';
el.querySelector('strong').textContent = data.title || '';
el.querySelector('p').textContent = data.text;
stack.prepend(el);
while (stack.children.length > MAX) stack.lastElementChild.remove();
setTimeout(() => el.remove(), data.duration);
});Setting textContent instead of innerHTML for the message stops scripts (or players, through chat-driven messages) from injecting HTML into your UI.
#notify { position: fixed; top: 3vh; right: 2vw; display: flex; flex-direction: column; gap: 0.8vh; width: 22vw; }
.toast { background: rgba(14, 14, 18, 0.9); border-left: 3px solid #4f8cff; border-radius: 8px; padding: 1vh 1vw; color: #eee; font-size: 1.5vh; }
.toast.success { border-color: #3ecf8e; }
.toast.error { border-color: #ff5d5d; }
.toast.warning { border-color: #ffb020; }Placement and behaviour
- Top-right or top-centre: the minimap is bottom-left and chat is usually top-left.
- 4–6 seconds for normal messages; longer for errors players must read.
- A short, quiet sound for errors only — constant sounds get muted.
- Hide notifications in the pause menu, like the rest of the HUD — see HUD design.
Routing framework notifications
| Source | How to redirect |
|---|---|
| ESX | Edit ESX.ShowNotification in es_extended’s client functions to call your export |
| QBCore | Edit QBCore.Functions.Notify in qb-core’s client functions (the QBCore:Notify event uses it) |
| ox_lib | Leave lib.notify as is and match its style, or wrap it in your own scripts |
| Your scripts | Call exports.mynotify:notify directly |
Common questions
How do I make custom notifications in FiveM?
Create a resource with a client export and event that send NUI messages to a stack of toast elements in your UI.
How do I replace ESX notifications?
Change ESX.ShowNotification in es_extended’s client functions to call your notification export.
How do I replace QBCore notifications?
Change QBCore.Functions.Notify in qb-core’s client functions to call your export.
How many notifications should show at once?
Around three to five; merge duplicates so spam does not fill the screen.
Want a loading screen you never have to debug?
Build it in the browser, export once, and edit it whenever — no HTML, no re-uploads.
Start free