Tutorial 2026-04-05

Building a Custom Notification System for FiveM

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

Why Build a Custom Notification System?

Default FiveM notifications are functional but lack visual polish and flexibility. Most roleplay servers rely on generic text popups that all look identical, making it hard for players to distinguish between an error, a success confirmation, or an informational alert. A custom notification system built with NUI gives you full control over styling, animations, positioning, sound effects, and queuing behavior. It transforms a basic UI element into something that matches your server's brand identity and significantly improves the player experience. In this tutorial, we will build a complete toast notification system from scratch using Lua on the server and client side, with HTML, CSS, and JavaScript powering the NUI frontend.

Setting Up the NUI Layer

The foundation of any custom notification system in FiveM is the NUI (New UI) layer. NUI allows you to render HTML content as an overlay on top of the game, which means you have the full power of modern web technologies at your disposal. Start by creating your resource structure with a fxmanifest.lua, a client-side Lua script, and an html folder containing your UI files. The manifest needs to declare your NUI page and register message handlers so your Lua scripts can communicate with the frontend.

-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

ui_page 'html/index.html'

files {
    'html/index.html',
    'html/style.css',
    'html/script.js',
    'html/sounds/*.ogg'
}

client_script 'client.lua'
server_script 'server.lua'

Your html/index.html file should be minimal. It only needs a container div where notifications will be dynamically injected by JavaScript. Keep the HTML lean because every notification element is created and destroyed programmatically. Link your stylesheet and script, and make sure the body has a transparent background so the game world remains visible behind your notifications.

<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="notification-container"></div>
    <script src="script.js"></script>
</body>
</html>

Designing the Toast Notification CSS

Toast notifications should be visually distinct by type. Define separate color schemes for success, error, info, and warning notifications. Position the container in the top-right corner of the screen using fixed positioning, and stack notifications vertically with a small gap between them. Each toast should have a subtle glassmorphism effect with backdrop blur, a colored left border to indicate the type, and smooth entrance and exit animations. Use CSS keyframes for slide-in from the right and fade-out when the notification expires.

/* html/style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; font-family: 'Segoe UI', sans-serif; overflow: hidden; }

#notification-container {
    position: fixed;
    top: 20px;
    right: 20px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    z-index: 9999;
    max-width: 380px;
    width: 100%;
}

.toast {
    background: rgba(15, 15, 25, 0.85);
    backdrop-filter: blur(12px);
    border-radius: 10px;
    padding: 14px 18px;
    border-left: 4px solid #3b82f6;
    color: #e2e8f0;
    animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

.toast.success { border-left-color: #22c55e; }
.toast.error   { border-left-color: #ef4444; }
.toast.warning { border-left-color: #f59e0b; }
.toast.info    { border-left-color: #3b82f6; }

.toast-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
.toast-body  { flex: 1; }
.toast-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.toast-msg   { font-size: 13px; color: #94a3b8; line-height: 1.5; }

.toast-progress {
    position: absolute;
    bottom: 0; left: 0;
    height: 3px;
    background: currentColor;
    border-radius: 0 0 0 10px;
    animation: progress linear forwards;
}

@keyframes slideIn {
    from { opacity: 0; transform: translateX(100px); }
    to   { opacity: 1; transform: translateX(0); }
}

@keyframes slideOut {
    from { opacity: 1; transform: translateX(0); }
    to   { opacity: 0; transform: translateX(100px); }
}

@keyframes progress {
    from { width: 100%; }
    to   { width: 0%; }
}

Building the JavaScript Notification Queue

The JavaScript layer handles incoming messages from Lua, creates DOM elements, manages the queue, and handles auto-dismissal. A notification queue is essential because you do not want ten notifications stacking on screen simultaneously. Limit the visible count to a maximum of five, and queue any overflow notifications so they appear as earlier ones expire. Each notification should have a configurable duration, and clicking a notification should dismiss it immediately. The progress bar at the bottom of each toast gives players a visual indicator of how long the notification will remain on screen.

// html/script.js
const container = document.getElementById('notification-container');
const MAX_VISIBLE = 5;
const queue = [];
let activeCount = 0;

const icons = {
    success: '✔',
    error:   '✖',
    warning: '⚠',
    info:    'ℹ'
};

const sounds = {
    success: new Audio('sounds/success.ogg'),
    error:   new Audio('sounds/error.ogg'),
    warning: new Audio('sounds/warning.ogg'),
    info:    new Audio('sounds/info.ogg')
};

window.addEventListener('message', (event) => {
    if (event.data.type === 'showNotification') {
        addNotification(event.data);
    }
});

function addNotification(data) {
    if (activeCount >= MAX_VISIBLE) {
        queue.push(data);
        return;
    }
    createToast(data);
}

function createToast(data) {
    activeCount++;
    const duration = data.duration || 5000;
    const toast = document.createElement('div');
    toast.className = `toast ${data.style || 'info'}`;
    toast.style.position = 'relative';
    toast.innerHTML = `
        <div class="toast-icon">${icons[data.style] || icons.info}</div>
        <div class="toast-body">
            <div class="toast-title">${data.title || ''}</div>
            <div class="toast-msg">${data.message}</div>
        </div>
        <div class="toast-progress" style="animation-duration:${duration}ms"></div>
    `;

    toast.addEventListener('click', () => dismissToast(toast));
    container.appendChild(toast);

    if (data.sound !== false && sounds[data.style]) {
        sounds[data.style].currentTime = 0;
        sounds[data.style].play().catch(() => {});
    }

    setTimeout(() => dismissToast(toast), duration);
}

function dismissToast(toast) {
    if (toast.dataset.dismissed) return;
    toast.dataset.dismissed = 'true';
    toast.style.animation = 'slideOut 0.3s ease forwards';
    setTimeout(() => {
        toast.remove();
        activeCount--;
        if (queue.length > 0) {
            createToast(queue.shift());
        }
    }, 300);
}

Client-Side Lua Integration

On the client side, you need a function that sends NUI messages to your HTML layer and an export so other resources can trigger notifications without directly depending on your script's internals. The SendNUIMessage native pushes data to the browser context where your JavaScript picks it up. Wrap this in a clean API function that accepts a title, message, type, and optional duration. Register both a client event and an export so notifications can be triggered from both server-side scripts and other client resources. This dual approach ensures maximum compatibility with any framework.

-- client.lua
local function ShowNotification(title, message, style, duration, sound)
    SendNUIMessage({
        type   = 'showNotification',
        title  = title or '',
        message = message or '',
        style  = style or 'info',
        duration = duration or 5000,
        sound  = sound ~= false
    })
end

-- Export for other resources
exports('ShowNotification', ShowNotification)

-- Event-based trigger from server
RegisterNetEvent('notifications:show', function(title, message, style, duration)
    ShowNotification(title, message, style, duration)
end)

-- Convenience commands for testing
RegisterCommand('testnotify', function()
    ShowNotification('Success', 'Your item has been saved.', 'success', 4000)
    Wait(500)
    ShowNotification('Error', 'Insufficient funds for this purchase.', 'error', 5000)
    Wait(500)
    ShowNotification('Warning', 'Your vehicle is low on fuel.', 'warning', 4000)
    Wait(500)
    ShowNotification('Info', 'Press E to interact with the NPC.', 'info', 3000)
end, false)

Server-Side Event Dispatch

The server-side script provides helper functions to send notifications to specific players, all players, or groups of players. This is where you handle use cases like broadcasting announcements, sending transaction confirmations, or alerting admins about suspicious activity. By centralizing the dispatch logic on the server, you maintain a single point of control over notification delivery. You can also add rate limiting here to prevent notification spam from malicious or buggy client scripts. The server should validate the notification parameters before forwarding to prevent NUI injection through crafted messages.

-- server.lua
local function NotifyPlayer(source, title, message, style, duration)
    if not source or source <= 0 then return end
    title   = tostring(title or '')
    message = tostring(message or '')
    style   = style or 'info'
    TriggerClientEvent('notifications:show', source, title, message, style, duration)
end

local function NotifyAll(title, message, style, duration)
    TriggerClientEvent('notifications:show', -1, title, message, style, duration)
end

exports('NotifyPlayer', NotifyPlayer)
exports('NotifyAll', NotifyAll)

-- Example: welcome notification
AddEventHandler('playerJoining', function()
    local src = source
    Wait(3000)
    NotifyPlayer(src, 'Welcome', 'Welcome to the server! Have fun.', 'success', 6000)
end)

-- Admin broadcast command
RegisterCommand('broadcast', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.broadcast') then return end
    local msg = table.concat(args, ' ')
    NotifyAll('Announcement', msg, 'info', 8000)
end, true)

Adding Sound Effects and Polish

Sound effects elevate notifications from a visual-only element to a multi-sensory feedback mechanism. Players often have the game in the background or are focused on driving and may miss a silent toast. A short, subtle chime for success notifications, a low buzz for errors, and a gentle ping for info alerts ensure players register important information even when they are not looking at the notification area. Keep your audio files short, under 500 milliseconds, and use OGG format for browser compatibility. Set the volume to around 30 percent so it blends with game audio rather than overwhelming it. Store the sound files in your html/sounds directory and preload them in JavaScript to avoid playback delays on the first notification.

Advanced Features and Customization

Once you have the core system working, consider adding advanced features to differentiate your server. Persistent notifications that stay on screen until the player explicitly clicks them are useful for important alerts like pending phone calls or jail timers. Action buttons inside notifications let players respond directly, for example accepting a trade request without opening a separate menu. You can also implement notification grouping where repeated identical notifications collapse into a single toast with a counter badge showing the number of occurrences. For framework integration, create bridge files that override the default notification functions in QBCore or ESX so every resource on your server automatically uses your custom system without any code changes. This pattern of replacing framework defaults with your own implementation is a clean way to upgrade an entire server's UI in one step.

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.