Features
Card anatomy
Each card is a round icon disc on the left and a translucent dark body on the right. The body has a vertical accent bar in the type colour on its left edge; the bar draws in over 0.4 s and then drains from full to empty over the card's duration, so the bar doubles as the timer. Inside the body: an uppercase, letter-spaced title at 48% opacity and a bold message.
The disc is exactly as tall as the body, and its width is kept equal to that height by a ResizeObserver, so the icon is always a perfect circle even when the message wraps to several lines.
Text is Arial / Helvetica. Sizes scale with the screen height and Config.Scale; the JS rounds them to whole pixels so the text stays crisp in the game's NUI compositor.
Positions and queues
Nine anchors, each an independent stack. Bottom anchors grow upwards, top anchors grow downwards. Every stack shows up to Config.MaxVisible cards; the rest wait in that stack's queue and are released as cards leave. The maximum card width is 940 units (1/1080 of the screen height each) or 86% of the viewport width, whichever is smaller.
Duplicate merging
A card is identified by its position, icon, colour, title and message. When an identical notification arrives while the first one is still on screen:
- no new card is created;
- the existing card gets a
×Ncounter next to the title, in the accent colour; - its timer and accent bar restart with the new duration;
- the card "bumps" (a 2.5% scale pulse);
- the sound plays again only if at least one second has passed since the last sound for that card.
Identical notifications waiting in the queue are merged the same way, and a queued item that arrives with a count is shown with the counter already set.
On top of that, the Lua side drops a call whose message, type and title equal the previous call's within Config.DedupeWindow (150 ms). That guard exists for the case where the same notification is delivered by two code paths at once; the player never sees it.
Timeline
| Phase | Duration |
|---|---|
| Enter (slide from the anchor side, disc pops, text slides in) | 0.45 s |
| Visible | duration (at least 1500 ms) |
| Exit (slide out and shrink) | 0.3 s, then the slot collapses over 0.3 s |
The entry class is removed 700 ms after the card appears so the browser stops compositing the text layer; only the accent bar keeps animating while the card is live.
Sound
Played by the NUI with an Audio element from html/sounds (or a URL). Priority: the call's sound field, then the type's sound key, then Config.Sound.file. Config.Sound.enabled = false silences everything; sound = false in a call silences one card. The volume is Config.Sound.volume, clamped to 0–1.
Pause menu
A client loop checks IsPauseMenuActive() every 250 ms and hides the whole notification layer while the pause menu is open. Timers keep running, so a card that expires while the menu is open is gone when the menu closes.
Layering above other NUI
FiveM stacks every resource's NUI page as a full-screen frame and re-assigns z-indices whenever a resource takes focus: the focused frame gets 99999 and the others 0, 1, 2 … in start order. A notification frame started early would end up under HUDs started later. gta6_notify therefore calls SetNuiZindex(Config.ZIndex) every 250 ms, keeping its cards above every unfocused frame and, on purpose, just under the focused one so a focused menu still receives mouse clicks. See Config.ZIndex in Configuration.