Bundling Custom UI with Your Mod
February 10, 2026 · View on GitHub
This guide explains how mods can override, extend, or fully replace the RmlUI menus and HUD.
How It Works
The engine uses a layered file search when loading any UI asset (RML documents, RCSS stylesheets, fonts). When RmlUI requests a file, the QuakeFileInterface checks directories in this order:
1. <mod_directory>/<path> ← highest priority
2. <basedir>/<path> ← fallback
This means a mod can override any UI file by placing a file at the same relative path inside its own directory. No configuration is needed — the engine finds the mod version automatically.
Quick Start
1. Set up your mod's quake.rc
// quake.rc — minimum
exec default.cfg
exec config.cfg
exec autoexec.cfg
stuffcmds
startdemos demo1 demo2 demo3
ui_show_when_ready
RmlUI menus and HUD are always active when the engine is compiled with USE_RMLUI (the default). No cvars needed to enable them.
2. Override only what you need
Create a ui/ tree inside your mod directory, mirroring the paths you want to replace:
mymod/
├── quake.rc
└── ui/
└── rcss/
└── main_menu.rcss ← custom main menu style
Everything you don't override falls through to the base ui/ directory. You can override a single stylesheet, a single menu, or the entire UI.
3. Run
make run MOD_NAME=mymod
# or
./build/vkquake -game mymod
For a concrete reference, see the in-repo example mod: ui_lab/ (make run MOD_NAME=ui_lab).
Mod Directory Layout
A fully customized mod might look like this (all ui/ entries are optional):
mymod/
├── quake.rc # Startup script
├── progs.dat # Compiled QuakeC (game logic)
├── qcsrc/ # QuakeC source (optional)
│ └── ...
└── ui/ # Custom UI overrides
├── rml/
│ ├── menus/
│ │ ├── main_menu.rml # Replaces base main menu
│ │ ├── pause_menu.rml # Replaces base pause menu
│ │ └── credits.rml # New menu (navigate to it from another menu)
│ └── hud/
│ └── hud.rml # Replaces base HUD
├── rcss/
│ ├── base.rcss # Replaces base styles (colors, fonts, etc.)
│ ├── menu.rcss # Replaces menu layout styles
│ ├── hud.rcss # HUD core + default HUD styles
│ ├── centerprint.rcss # Centerprint banner styles
│ ├── notify.rcss # Notify message styles
│ ├── chat.rcss # Chat input styles
│ ├── scoreboard.rcss # Scoreboard overlay styles
│ └── intermission.rcss # Intermission/finale styles
└── fonts/
└── MyCustomFont.ttf # Replaces a base font (same filename)
If you want the mod to appear in the in-game Mods menu, make sure the mod directory has at least one of:
pak0.pak, progs.dat, csprogs.dat, maps/, or ui/.
Automatic Mod Branding
The {{ game_title }} data binding automatically displays your mod's directory name. If your mod directory is mymod/, the main menu title shows "MYMOD" — no hardcoding required.
<h1>{{ game_title }}</h1> <!-- displays "MYMOD" -->
What You Can Override
| Asset Type | Base Path | Override by placing in... |
|---|---|---|
| Menu documents | ui/rml/menus/*.rml | mymod/ui/rml/menus/*.rml |
| HUD document | ui/rml/hud/hud.rml | mymod/ui/rml/hud/hud.rml |
| HUD overlays | ui/rml/hud/scoreboard.rml, intermission.rml | mymod/ui/rml/hud/*.rml |
| Stylesheets | ui/rcss/*.rcss | mymod/ui/rcss/*.rcss |
| Fonts | ui/fonts/*.ttf | mymod/ui/fonts/*.ttf |
Base Stylesheets
| File | Purpose |
|---|---|
base.rcss | Reset, typography, color variables, animations |
menu.rcss | Menu panels, navigation, layout |
main_menu.rcss | Main menu specific styles |
hud.rcss | HUD core (overlay, positioning, crosshair, level stats) + default HUD styles (weapon bar, health/armor/ammo corners, keys, powerups) |
centerprint.rcss | Centerprint banner animations and styles |
notify.rcss | Notify message line styles |
chat.rcss | Chat input overlay styles |
scoreboard.rcss | Scoreboard table overlay |
intermission.rcss | Intermission stats and finale text |
widgets.rcss | Form elements (sliders, checkboxes, dropdowns) |
HUD
The default HUD is a clean, modern corner-based layout at ui/rml/hud/hud.rml:
- Bottom-left: Health + armor (with armor-type color coding)
- Bottom-center: Weapon bar (8 slots — not owned/owned/active states)
- Bottom-right: Weapon label + ammo count + context-sensitive ammo reserves
- Top-left: Powerup badges, level stats (kills/secrets), notify messages
- Top-right: Key indicators (silver/gold)
The HUD links multiple RCSS files (hud.rcss, centerprint.rcss, notify.rcss, chat.rcss). Override any of them individually or replace the entire hud.rml for a fully custom HUD.
Writing Custom Menus
Minimal RML Menu
<rml>
<head>
<title>My Menu</title>
<link type="text/rcss" href="../../rcss/base.rcss" />
<link type="text/rcss" href="../../rcss/menu.rcss" />
</head>
<body data-model="game">
<h1>{{ game_title }}</h1>
<button class="btn-primary" onclick="new_game()">
NEW GAME
</button>
<button class="btn" onclick="navigate('options')">
OPTIONS
</button>
<button class="btn" onclick="quit()">
QUIT
</button>
</body>
</rml>
Available Actions
Use these in onclick attributes:
| Action | Description |
|---|---|
navigate('menu') | Push a menu onto the stack (ui/rml/menus/<menu>.rml) |
command('cmd') | Execute a console command (e.g., command('map e1m1')) |
close() | Pop the current menu |
close_all() | Close all menus, return to game |
new_game() | Start a new game |
quit() | Quit the game |
load_game('slot') | Load a saved game |
save_game('slot') | Save the current game |
cycle_cvar('name', n) | Cycle a cvar value by n |
Data Bindings
All RML documents have access to these bindings via data-model="game":
Game State (read-only, updated each frame):
{{ health }}, {{ armor }}, {{ ammo }}, {{ shells }}, {{ nails }}, {{ rockets }}, {{ cells }}, {{ map_name }}, {{ level_name }}, {{ game_title }}, {{ game_time }}
Conditional rendering:
<div data-if="has_quad">QUAD DAMAGE!</div>
<div data-if="health < 25">LOW HEALTH</div>
Cvar bindings (two-way, via data-model="cvars"):
<input type="range" data-value="sensitivity" min="1" max="20" step="0.5" />
See DATA_CONTRACT.md for the full list of available bindings.
RmlUI Constraints
A few things to keep in mind when authoring RML/RCSS:
rgba()alpha is 0-255, not 0-1 — e.g.rgba(255, 0, 0, 128)for 50% red. Hex-alpha also works (#FF000080)- No
font-effect: glow()— useoutline()orshadow()only - Logical operators are C-style — use
&&,||,!(notand/or/not) - XML escaping in attributes —
&&becomes&&,<becomes< - Use
position: absolutefor HUD overlay elements
See the /rmlui skill reference for workflow details and ../.claude/skills/rmlui/*.md for syntax examples.
Hot Reload for Development
While the engine is running, you can reload UI assets without restarting:
| Console Command | Effect |
|---|---|
ui_reload | Full reload — clears document cache, reloads all RML + RCSS |
ui_reload_css | Lightweight — reloads RCSS only, preserves DOM and data bindings |
Workflow: edit your mod's RML/RCSS files, switch to the game, type ui_reload_css in the console.
UI-Related Cvars
| Cvar | Default | Description |
|---|---|---|
scr_uiscale | 1.0 | UI scale factor (0.5–3.0) |
Example: Style-Only Override
The simplest customization — override just the main menu colors without touching any RML:
mymod/
├── quake.rc
└── ui/
└── rcss/
└── main_menu.rcss
Your main_menu.rcss is a complete replacement for the base file, so copy the original and modify it.
Example: Custom Main Menu
Replace the main menu layout entirely:
mymod/
├── quake.rc
└── ui/
└── rml/
└── menus/
└── main_menu.rml
Your replacement main_menu.rml can link to base stylesheets (they'll resolve from the base ui/ directory since you didn't override them), or you can bundle your own.