Overview
Sal's Kewl Cards puts reference material on screen without making anyone alt-tab for it. A card is one page of content a player can pull up mid-scene: a Miranda card, a vitals chart, a fire color-code table, a department SOP, a price list.
Three things make it more than an image viewer:
- Cards can be text, not just images. A text card is typed
straight into
config.lua. It downloads nothing, stays sharp at any zoom, and you can edit one without opening an image editor. - Cards are gated by job and grade. Police SOPs stay with police; supervisor material stays with supervisors.
- You can show a card to another player. Hold the Miranda card up to the suspect in front of you and they see it on their screen.
It works on qb-core, Qbox, ESX and standalone. Detection is automatic and there is nothing to configure for it.
Requirements
| Required | ox_lib — the menu and notification library. This is the only hard dependency. |
|---|---|
| Framework | qb-core, Qbox (qbx_core) or ESX (es_extended). None of them is required — with no framework present every player is treated as a civilian and open cards still work. |
| Optional | ox_inventory or qb-inventory, only if you turn on the inventory item. Detected at runtime; never listed as a dependency, so the resource starts on servers that do not run them. |
No database. No permanent server state. Favorites and recents are stored in each player's own FiveM key-value store on their machine.
Installation
- Drop the folder into your
resources/directory assals_kewlcards. - Add it to
server.cfg, after ox_lib:ensure ox_lib ensure sals_kewlcards - Open
config.luaand change thejobson each card to your server's job names. The shipped values —police,sasp,bcso,lspd,safr— are examples, not defaults that will match your server. - Restart the server and run
/notecardsin game.
If the menu says "No notecards available for your job", step 3 is
almost always why. Check your actual job name with your framework's own
command and compare it to the jobs list on the card.
Upgrading from 1.0
2.0.0 is a rewrite. Your old card entries still work, but two things moved:
- Images live in
nui/images/, notnui/stream/. Copy your own artwork into the new folder. (A folder namedstreamis FiveM's convention for streamed game assets, which notecards are not.) - The menu command is
/notecards, not/notecardmenu. Both command names are set inConfig.Commandsif you would rather keep the old one.
Cards also gained category, order,
text and images fields. All four are optional — a
1.0-style card with just title, image,
description and jobs still loads.
If a card is misconfigured, the resource says so in the client console at startup, names the card, and says whether the problem was missing content or a rejected filename. It does not fail silently.
Player guide
This is the part to paste into your server rules or a Discord channel.
| Command | What it does |
|---|---|
/notecards | Open the card menu |
/shownotecard <name> | Open one card directly, e.g. /shownotecard miranda |
/showcardto <id> <name> | Show a card to another player by server ID |
/cardsunstick | Escape hatch — releases the mouse cursor if the UI ever hangs |
Keybind
The menu is bindable and ships unbound, so players choose
their own key in Settings → Key Bindings → FiveM → Open the
notecard menu. To preset one for everybody, put a key name in
Config.Keybind.
In the viewer
| Input | Action |
|---|---|
| Esc or Backspace | Close the card |
| ← → | Flip pages on a multi-page card |
| Scroll wheel | Zoom in and out |
| Drag | Pan a zoomed card |
| Double-click | Reset zoom |
| Star icon | Pin the card to favorites |
| People icon | Show the card to the nearest player |
A card does not freeze you. With
Config.Viewer.KeepInput on (the default) a player reading a card
can still walk, run and talk — which is the point, since you read a
Miranda card at somebody. Camera-look and the pause menu are held off
while a card is up so neither fights the mouse cursor.
Writing cards
Every card is one entry in Config.Notecards. The key is the
shortcut players type with /shownotecard, so keep it short and
lowercase.
Text cards
Start here. A text card needs no artwork, downloads nothing, and stays sharp at any zoom level:
['callsigns'] = {
title = 'Callsign Format',
description = 'How to build your unit number.',
category = 'general',
order = 10,
jobs = {},
text = [=[
# Format
- **1-ADAM-12** — district, unit type, beat
- Supervisors use **S** in the unit slot
Say your full callsign on first contact, short form after.
]=],
},
The markup is deliberately tiny:
| Write | Get |
|---|---|
# Heading | A section heading |
- item | A bullet (* works too) |
**bold** | Bold text |
| A blank line | A new paragraph |
Nothing else is interpreted. Lines wrapped in your editor rejoin into one
paragraph, so you can keep config.lua tidy without changing how a
card reads.
Card text is never treated as HTML. A card containing
<script> displays those characters as text, because the
viewer builds the page out of text nodes rather than pasting markup into it.
Image cards
Put the file in nui/images/ and name it in the card:
['usecodes'] = {
title = 'Use of Force',
description = 'Continuum and reporting thresholds.',
category = 'law',
order = 20,
image = 'usecodes.webp',
jobs = { 'police', 'bcso' },
},
Multi-page cards
Swap image for images and the viewer grows page
controls and arrow-key navigation:
images = { 'pursuit_p1.webp', 'pursuit_p2.webp' },
Every field
| Field | Required | What it does |
|---|---|---|
title | No | Shown in the menu and at the top of the card. Defaults to the key. |
description | No | The line under the title in the menu. |
text | One of these | Card content as text. |
image | One image filename from nui/images/. | |
images | A list of filenames, shown as pages. | |
jobs | No | Who may open it. Omit for everyone. See permissions. |
category | No | Which menu section it lands in. Unknown or missing goes to Config.FallbackCategory. |
order | No | Sort position, low to high. Defaults to 50. Ties break by title, then key. |
Job & grade permissions
The jobs field takes three shapes.
jobs = nil -- everyone, including civilians
jobs = {} -- everyone
jobs = { 'police', 'sasp' } -- those jobs, any grade
jobs = { police = 3, sasp = 3 } -- those jobs, grade 3 or higher
The last form is how you keep supervisor material with supervisors. The
number is a minimum: police = 3 means grade 3 and
everything above it.
Grades are read from whatever your framework reports — qb and Qbox
nest it as job.grade.level, ESX gives a plain number, and both are
normalized to the same thing before the check runs.
The same permission function decides what the menu offers and whether the server will let a player present a card to someone else. It is written once and loaded on both sides, so the two cannot drift apart as you edit your config.
Showing a card to a player
This is the feature that makes the resource a scene tool rather than a reference binder. Two ways in:
- The people icon in the viewer shows the card to the nearest player within range.
/showcardto <id> <name>targets a specific player by server ID.
The receiving player sees the card with a banner naming who showed it to
them. They can close it themselves, or set
Config.Present.Duration to close it automatically after a number
of milliseconds.
What the server checks
Every present request is validated on the server, against state the server owns rather than anything the sending client claimed:
- The card exists in your config. The client sends a card key, never an image path, so nothing a client says can put an arbitrary file on someone else's screen.
- The presenter's job passes that card's own gate —
resolved server-side. Turn this off with
Config.Present.RequireJob = falseif you want any player to be able to hand any card around. - The distance between the two characters is within
Config.Present.Distance, measured from the server's own entity positions. - A per-player cooldown (
Config.Present.Cooldown) so the feature cannot be used to spam someone's screen.
Set Config.Present.Enabled = false to turn the whole feature
off.
Categories, favorites & search
Categories
Cards group into categories so the menu stays usable past a dozen or so.
Define them in Config.Categories:
Config.Categories = {
{ id = 'law', label = 'Law Enforcement', icon = 'shield-halved', order = 10 },
{ id = 'fire', label = 'Fire', icon = 'fire-extinguisher', order = 20 },
{ id = 'ems', label = 'EMS', icon = 'briefcase-medical', order = 30 },
{ id = 'general', label = 'General', icon = 'book', order = 99 },
}
Icons are Font Awesome names, the same set ox_lib uses. A category with no cards the player may open is hidden rather than shown empty. If a player ends up with exactly one category and no favorites or recents, the menu skips the middle step and lists the cards directly.
Favorites and recents
Players pin cards with the star in the viewer, and the last few they opened appear under Recently Used. Both are stored per player on their own machine and survive a reconnect. Both are pruned automatically when a card is renamed or removed, and both respect the job gate — a pinned card the player no longer has access to simply stops appearing.
Caps are Config.Favorites.Max and Config.Recent.Max.
Set Enabled = false on either to remove it from the menu.
Search
Search appears once a player can see Config.Search.MinCards
cards, so a small catalog does not carry a search box it does not need. It
matches on title, description and shortcut, and only ever returns cards that
player is allowed to open.
Card artwork
WebP, PNG and JPG all work. WebP is worth using: every player downloads every card image when they join, whether or not their job can open it.
Two rules for artwork you draw yourself:
- 1000px wide or more. The viewer zooms up to 4x, and zoom can only enlarge detail that is in the file. The three cards that ship with the resource are 1000×600.
- Plain filenames. Letters, digits, dots, dashes and underscores only — no folders, no spaces. Anything else is rejected at startup and the console says which card and why.
If a card is mostly words, do not draw it at all. Make it a text card: sharp at every zoom level, zero download, and editable in the same file as the rest of your config.
Optional inventory item
Off by default. Turn it on only after adding the item to your inventory resource:
Config.Inventory = {
Enabled = true,
Item = 'notecard_binder',
}
On qb-core, Qbox and ESX the useable item is registered for you. On
ox_inventory, items are driven from its own items.lua
— point the item's client.export at
sals_kewlcards.openMenu.
If no supported inventory resource is found, the item check passes rather than failing. Locking every player out of their reference cards because an inventory export moved is a worse outcome than a missed item check.
Configuration reference
| Key | Default | What it does |
|---|---|---|
Config.Debug.Enabled | false | Console tracing. Ships off; turn on only while developing. |
Config.Locale | 'en' | Which file in locales/ to use. |
Config.Commands.Menu | 'notecards' | Command that opens the menu. |
Config.Commands.Show | 'shownotecard' | Command that opens one card. |
Config.Commands.Present | 'showcardto' | Command that shows a card to a player. |
Config.Keybind | '' | Default key for the menu. Empty ships unbound. |
Config.Present.Enabled | true | Whether cards can be shown to other players at all. |
Config.Present.Distance | 6.0 | Meters. Checked on the server. |
Config.Present.Cooldown | 3000 | Milliseconds between presents, per player. |
Config.Present.RequireJob | true | Presenter must pass the card's own job gate. |
Config.Present.Duration | 0 | Milliseconds before it closes for the receiver. 0 means they close it. |
Config.Favorites.Enabled | true | The star, and the Favorites menu entry. |
Config.Favorites.Max | 8 | How many a player may pin. |
Config.Recent.Enabled | true | The Recently Used menu entry. |
Config.Recent.Max | 5 | How many are remembered. |
Config.Search.Enabled | true | The Search menu entry. |
Config.Search.MinCards | 8 | Visible cards needed before Search appears. |
Config.Inventory.Enabled | false | Require an item to open the menu. |
Config.Inventory.Item | 'notecard_binder' | Which item. |
Config.Viewer.KeepInput | true | Player can still move and talk with a card up. |
Config.Viewer.Zoom | true | Scroll to zoom, drag to pan. |
Config.Viewer.MaxZoom | 4.0 | Zoom ceiling. |
Config.FallbackCategory | 'general' | Where a card with an unknown category lands. |
Exports
For driving the viewer from another resource — a CAD, a phone, a training script.
Client
exports.sals_kewlcards:openMenu()
exports.sals_kewlcards:openCard('miranda') -- honors the job gate; returns a boolean
exports.sals_kewlcards:isOpen() -- boolean
openCard returns false and shows nothing if the
key is unknown or the player's job does not allow it.
Server
exports.sals_kewlcards:showCard(playerId, 'miranda', 'Dispatch')
Puts a card on a player's screen with a banner naming the third argument as the sender. Returns a boolean. This is the admin/system path and does not apply the job gate — it is for your own trusted resources, not for player input.
What the job gate does
Worth being plain about, because it decides what belongs on a card.
Image cards are ordinary web assets. Every player who joins
downloads every image the resource ships, whether or not their job can open it.
The jobs field decides what the menu offers a player and what the
server will let them present to somebody else. It does not stop a determined
player from finding the file.
For reference material — rights cards, protocol charts, code tables — that is the right trade, and it is what this resource is for. But do not put anything on a card that you would mind any player seeing.
Text cards behave the same way: they live in config.lua, which
is delivered to clients as part of the resource.
Translating
No player-facing string is hardcoded. locales/en.lua is the
baseline; copy it, translate the values, and point
Config.Locale at the new file:
Locales['fr'] = {
['menu_title'] = 'Fiches',
['none_available'] = 'Aucune fiche disponible pour votre metier.',
-- ...
}
Anything you leave out falls back to English rather than showing a blank, so
a partial translation is safe to ship. Card title and
description text lives in config.lua with the cards
themselves, not in the locale file.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| "No notecards available for your job" | The jobs lists in config.lua do not match your
server's real job names. The shipped values are examples. Check your actual
job name and compare. |
| A card is missing from the menu for one person | Grade, not job. If the card uses the { police = 3 } form,
anyone below grade 3 will not see it. |
| A card is missing for everybody | Check the client console at startup. A card with no content, or with a rejected image filename, is named there along with which of the two it was. |
| The card is blank, or the image does not load | The file is not in nui/images/, or its name is not in the
manifest's files block. The manifest globs
nui/images/*.webp, *.png and *.jpg
— any other extension needs adding there. |
| The mouse cursor is stuck on screen | Run /cardsunstick. This should not happen — the
viewer releases the cursor on close, on resource stop, and via a watchdog if
the UI fails to load — but the command is there regardless. If it
happens repeatedly, turn on Config.Debug.Enabled and check the
client console (F8). |
| Escape opens the pause menu instead of closing the card | Something else is disabling this resource's control suppression. Set
Config.Viewer.KeepInput = false as a workaround; the card will
then hold input the way a normal menu does. |
| "You need a notecard_binder" | Config.Inventory.Enabled is on but the item does not exist
in your inventory resource. Add it, or set that back to false. |
| Showing a card to a player does nothing | Distance, cooldown or job. The server tells the sender which one it
was. If nothing appears at all, check Config.Present.Enabled. |
| Menu order keeps changing | It should not — cards sort by order, then title, then
key. If two cards have the same order they sort alphabetically
by title, which may not be the order you expected. Set
order explicitly. |
Support
Support, release news and coupon codes are on Discord. Purchases are handled by Tebex, who own the checkout, billing support and refunds. The resource ships with its own README and CHANGELOG — the CHANGELOG calls out breaking config changes explicitly.
All pricing is on the main site.