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 TypeBase PathOverride by placing in...
Menu documentsui/rml/menus/*.rmlmymod/ui/rml/menus/*.rml
HUD documentui/rml/hud/hud.rmlmymod/ui/rml/hud/hud.rml
HUD overlaysui/rml/hud/scoreboard.rml, intermission.rmlmymod/ui/rml/hud/*.rml
Stylesheetsui/rcss/*.rcssmymod/ui/rcss/*.rcss
Fontsui/fonts/*.ttfmymod/ui/fonts/*.ttf

Base Stylesheets

FilePurpose
base.rcssReset, typography, color variables, animations
menu.rcssMenu panels, navigation, layout
main_menu.rcssMain menu specific styles
hud.rcssHUD core (overlay, positioning, crosshair, level stats) + default HUD styles (weapon bar, health/armor/ammo corners, keys, powerups)
centerprint.rcssCenterprint banner animations and styles
notify.rcssNotify message line styles
chat.rcssChat input overlay styles
scoreboard.rcssScoreboard table overlay
intermission.rcssIntermission stats and finale text
widgets.rcssForm 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:

ActionDescription
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 &lt; 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() — use outline() or shadow() only
  • Logical operators are C-style — use &&, ||, ! (not and/or/not)
  • XML escaping in attributes&& becomes &amp;&amp;, < becomes &lt;
  • Use position: absolute for 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 CommandEffect
ui_reloadFull reload — clears document cache, reloads all RML + RCSS
ui_reload_cssLightweight — 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.

CvarDefaultDescription
scr_uiscale1.0UI 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.