UI Scaling
Last updated
May be stale
This page was last updated ago, on 19 September 2026. Check what it says against the code before relying on it.
The cityrp.ui library (gamemode/core/libraries/cl_ui.lua) is the mandatory way to scale client UI — Derma panels, entity menus, HUD overlays, and any other screen-space layout or drawing. It combines automatic resolution scaling (1920×1080 reference) with the player's cl_llhud_scale preference and keeps every consumer in sync when either input changes.
When to use it
Use cityrp.ui for any client-side UI dimension that should track screen size or the player's scale setting:
- Panel sizes, margins, padding, and positions
- Font sizes
- Icon and button dimensions
draw.*coordinates in customPaintfunctions
Do not roll your own scale factor. Ad-hoc ScrW() / 1920 math, calling LimelightHUD.GetScaleFactor() directly, or hardcoded pixel values that never rebuild on scale change are legacy patterns — they drift from the HUD, ignore cl_llhud_scale, and break when resolution changes mid-session.
Enforcement: new or modified lines that introduce ScrW() / 1920, ScrH() / 1080, or
LimelightHUD.GetScaleFactor() fail standards-diff.py in CI (legacy core/derma/cl_hud_*.lua
panels are grandfathered until migrated). See Coding Standards.
Core API
| Function | Purpose |
|---|---|
cityrp.ui.Scale(value) |
Scale a single pixel measurement to the nearest whole pixel. Use for widths, heights, margins, positions, and any other screen-space dimension. |
cityrp.ui.GetScaleFactor() |
Return the raw, unrounded scale multiplier. Use only when you need the factor itself (e.g. proportional math before a single final round), not for individual pixel measurements — prefer Scale() for those. |
cityrp.ui.CreateScaledFont(name, baseFont, baseSize[, weight]) |
Create a font at the current scale. baseSize is the size at 1920×1080. |
cityrp.ui.OnScaleChanged(id, callback) |
Register a callback that runs immediately, then again (debounced) whenever cl_llhud_scale or screen resolution changes. Use to build and rebuild fonts and relayout panels. The callback must be safe to run at registration time. |
cityrp.ui.Shade(colour, amount) |
Lighten or darken a colour for hover/pressed states. Cache the result at style time, not inside Paint. |
cityrp.ui.ScaleCheckBox(checkbox, font) |
Apply a scaled font to a DCheckBoxLabel and grow its tick box to match. |
cityrp.ui.AddTitleBarCloseButton(frame, titleBarHeight, onClose) |
Add the shared text-only close button used on CityRP menu title bars. |
Panel kit
cityrp.ui also carries the panel kit — StyleFrame, StyleButton, ButtonColours,
StyleList, StyleCard, PaintCard, and the FONT_TITLE / FONT_BODY / FONT_CAPTION fonts.
Those are a matter of values rather than scaling, so they are documented in
UI Style, which names the helper for each pattern.
Standard patterns
Scale dimensions
local pad = cityrp.ui.Scale(10)
frame:SetSize(cityrp.ui.Scale(400), cityrp.ui.Scale(300))
button:DockMargin(pad, pad, pad, pad)
Scaled fonts
OnScaleChanged runs the callback once at registration, so a single registration both builds the fonts and keeps them in sync — there is no separate load-time call:
cityrp.ui.OnScaleChanged("mymenu", function()
cityrp.ui.CreateScaledFont("MyMenuTitle", "Roboto", 17, 500)
cityrp.ui.CreateScaledFont("MyMenuBody", "Roboto", 13, 400)
end)
That first call is synchronous, so the fonts exist by the line after the registration.
Relayout on scale change
If a panel's geometry depends on scaled values, rebuild or relayout inside the OnScaleChanged callback — not only the fonts. Menus that watched only cl_llhud_scale but not resolution kept the sizes they were built with when the screen size changed underneath them; OnScaleChanged watches both.
Because the callback also runs at registration, a relayout handler must guard its own preconditions rather than assume the thing it relayouts already exists:
cityrp.ui.OnScaleChanged("mymenu.layout", function()
if not IsValid(panel) then return end
panel:SetPos(cityrp.ui.Scale(16), cityrp.ui.Scale(16))
end)
What not to do
| ❌ Forbidden | ✅ Use instead |
|---|---|
ScrW() / 1920 (or similar resolution ratios) |
cityrp.ui.Scale() / cityrp.ui.GetScaleFactor() |
LimelightHUD.GetScaleFactor() in new code |
cityrp.ui.GetScaleFactor() |
surface.CreateFont with a fixed size for UI text |
cityrp.ui.CreateScaledFont |
| Hardcoded pixel sizes with no scale rebuild | cityrp.ui.Scale() + cityrp.ui.OnScaleChanged |
cvars.AddChangeCallback("cl_llhud_scale", ...) alone |
cityrp.ui.OnScaleChanged (also watches resolution) |
Existing HUD derma under gamemode/core/derma/cl_hud_*.lua still calls LimelightHUD.GetScaleFactor() directly — that predates cityrp.ui. New or refactored UI code must go through the library.