CityRP Documentation Repository

UI Style

Last updated

Which colour, font, size and pattern to use when you build client UI: a Derma panel, an entity menu or a HUD element. It is the style CityRP's recent panels already share, written down and ratified, so that a new panel matches the ones around it.

This page covers values. How sizes are scaled is in UI Scaling and is not repeated here: every dimension and font on this page goes through cityrp.ui unless it says otherwise.

Most of the patterns below are a call, not a recipe: cityrp.ui carries the panel kit, and the pattern sections name the helper that builds each one. Read the values so you know what the kit is doing and where a one-off has to match it by hand.

The rules apply to new UI and to UI you are already changing. They are not a reason to sweep old panels. The evidence behind each choice is in the repository at docs/design/ui-style/research-existing-ui.md.

Scope

  • Panels: anything the player opens. Frames, entity menus, editors, dialogs. Staff and developer panels use exactly the same style as player panels; there is no staff chrome.
  • HUD: anything drawn over the world while playing. Chips, badges, notifications, overlays.
  • The F1 menu follows this page in everything except scaling, until its replacement lands. The replacement's surface palette is being decided with that work and is not defined here.
  • Vendored addons are out of scope. Patch their integration points, do not re-skin them.

Exceptions

Two kinds of colour are outside the UI palette, and the frame chrome around them still follows this page:

  • Data palettes. Colours that encode a category: job and team colours, rank colours, chat channel colours, waypoint types.
  • Diegetic surfaces. The content area of something that is an object in the world: a card table, a keypad screen, a licence plate.

Further exceptions can be made for a specific theme. Write the reason down beside the palette.

Palette

A colour that has a role on this page is used by its constant, never retyped as Color(...). Inline Color(...) stays fine for a one-off that has no role here.

Panel surfaces

Neutral charcoal, opaque.

Role Constant Value
Frame fill COL_PANEL_BG 28, 28, 31, 252
Title bar COL_PANEL_TITLEBAR 20, 20, 23
Content surface COL_PANEL_BODY 38, 38, 42
Card, list row COL_PANEL_CARD 32, 32, 36
Alternate row COL_PANEL_ROW_ALT 40, 40, 45
List header, inset border COL_PANEL_HEADER 24, 24, 27
Text input COL_PANEL_INPUT 48, 48, 52
Neutral button COL_PANEL_BUTTON 58, 58, 64
Selected row COL_PANEL_SELECTED 24, 96, 168

Text

Role Constant Value
Body text COL_TEXT 206, 212, 218
Secondary text COL_TEXT_MUTED 132, 137, 145
Headings, labels on light-text fills COL_STEEL_WHITE 236, 239, 241
Labels on dark-text fills COL_STORM 33, 33, 33

Roles

Five semantic roles. Each is one colour, used for fills, rules, icons and text alike.

Role Constant Value Use
Brand COL_BRAND 66, 165, 245 The single accent: title-bar rule, active tab, primary button, focused input, text selection
Information COL_INFO (COL_UI_PALEBLUE) 112, 145, 187 Neutral emphasis, hints. Never a second accent
Success COL_SUCCESS (COL_UI_GREEN, COL_ACCEPT) 89, 127, 88 Confirm, save, met
Warning COL_WARNING (COL_UI_AMBER) 212, 172, 58 Needs attention, validation
Danger COL_DANGER (COL_UI_RED) 179, 74, 78 Destructive action, error, unmet

COL_REJECT (139, 35, 35) is danger as a large fill only, such as a banner.

Text on a role fill

Look the label colour up from the role; do not choose it by eye.

button:SetTextColor(COL_TEXT_ON[COL_BRAND])
Fill Label
COL_BRAND, COL_INFO, COL_WARNING COL_STORM (dark)
COL_SUCCESS, COL_DANGER, COL_PANEL_BUTTON, COL_STEEL COL_STEEL_WHITE (light)

COL_TEXT_ON is keyed by the colour itself, so look up the base role colour, once, when you style the control. A hover or pressed shade is a new Color and is not in the table. Labels on COL_SUCCESS are weight 500 or heavier.

States are derived

Hover, pressed and disabled colours are never authored. Build them once, at style time, with cityrp.ui.Shade, and never inside Paint:

State Shade
Hover cityrp.ui.Shade(base, 25)
Pressed cityrp.ui.Shade(base, -25)
Disabled cityrp.ui.Shade(base, -30), with the label switched to COL_TEXT_MUTED

Every interactive control has a disabled state. Paint in the order pressed, disabled, hovered, normal. Never write to a shared colour; Shade and ColorAlpha return a new one.

HUD

Role Constant Value
Chip with text COL_HUD_CHIP 41, 41, 41, 204
Icon tile COL_UI_GREY 41, 41, 41, 230
Chip outline, 1px COL_HUD_OUTLINE 68, 68, 68
World hint text ("Press E to...") COL_GUIDE 255, 137, 0

COL_GUIDE is for prompts drawn in the world through GM:DrawInformation. It is not a panel accent.

Type

All fonts are created with cityrp.ui.CreateScaledFont inside a cityrp.ui.OnScaleChanged callback; see UI Scaling.

Surface Face
Panels Roboto
HUD Saira
Chat log Tahoma, the one exception. It was chosen to avoid synthesised-bold blur at small sizes

Roboto ships with Garry's Mod and Saira is deployed with the server content. No other face is used: a face that is not deployed falls back silently on clients that do not have it installed.

Panel scale (Roboto)

Role Constant Size / weight Use
Title cityrp.ui.FONT_TITLE 19 / 500 Frame heading
Body cityrp.ui.FONT_BODY 14 / 400 Everything by default: labels, rows, buttons, inputs
Caption cityrp.ui.FONT_CAPTION 12 / 500 Secondary lines, list headers, hints
Section — 13 / 700 Optional. A group heading inside a dense table

The three constants are the kit's own fonts, built and rebuilt with the scale. A panel that needs none of its own names them rather than creating a duplicate set.

HUD scale (Saira)

Role Size Use
Label 19 Chip label
Secondary 15 Secondary chips, notifications
Caption 12 Captions, percentages

Weights 400 to 500. A label that must fit a fixed width may step down through smaller sizes; that is a fitting technique, not extra steps on the scale.

Naming

CityRP.<Feature>.<Role>, with the role taken from the tables above: CityRP.Phonebook.Title, CityRP.Phonebook.Body. Never name a font after its face.

Spacing and sizing

Units passed to cityrp.ui.Scale:

Unit Use
2 Rules, accent bars
4 Gaps inside a control, between a label and its control
8 The default gap between controls and between rows
12 The default panel padding; title inset
20 Content inset of a modal

6 is tolerated between tightly packed controls. Scale a padding once into a local and reuse it across DockMargin's arguments.

Element Size
Title bar Scale(26) for a single-purpose popup, Scale(32) for a multi-section frame
Button Scale(28) tall
List row Scale(22)
List header Scale(24)
Tab bar Scale(32)
Small entity popup Scale(400) by Scale(180)

Corner radius is not scaled. 6 for the frame and its title bar, 4 for everything inside: buttons, cards, rows, inputs. HUD chips are square.

A line that must never disappear is guarded: math.max(cityrp.ui.Scale(2), 1).

HUD elements keep a 30px screen margin, 8px between stacked elements and 4px inside a stack, and ask their neighbour for its bounds (Get*Bounds()) instead of hardcoding a y position.

Patterns

Frame

cityrp.ui.StyleFrame(frame, heading), which returns the title bar height to inset the content by. It does the title bar, the close button and the chrome below in one call.

A DFrame with SetTitle("") and ShowCloseButton(false), because the stock title and close button do not scale. Paint draws draw.RoundedBox(6, ...) in COL_PANEL_BG. Frames are centred and not draggable. SetBackgroundBlur(true) on a modal that blocks play; off on a tool used while playing.

Give a transparent child an explicit no-op Paint, never nil. Lay out with Dock, not SetPos.

Title bar

Part of cityrp.ui.StyleFrame; its third argument is the height, Scale(32) by default and Scale(26) for a single-purpose popup.

draw.RoundedBoxEx(6, ...) rounding the top corners only, in COL_PANEL_TITLEBAR, with a math.max(Scale(2), 1) rule in COL_BRAND along its bottom edge. The heading is the Title font in COL_STEEL_WHITE, inset Scale(12), vertically centred.

Fill the bar with a role colour only when the fill carries state, as the item grant menu does for give and take. Decoration is not a reason.

Close button

cityrp.ui.AddTitleBarCloseButton(frame, titleBarHeight, onClose), always.

Button

cityrp.ui.StyleButton(button, variant). A DButton with a Paint override: draw.RoundedBox(4, ...) in the state colour, Body font, label colour from COL_TEXT_ON.

Variant Argument Base
Primary "accent" COL_BRAND
Neutral "neutral" COL_PANEL_BUTTON
Positive "positive" COL_SUCCESS
Destructive "danger" COL_DANGER

One primary button per view.

A control that has to match a button but cannot be one -- a tab, a chip, a hand-built row -- takes its fills from cityrp.ui.ButtonColours(variant) rather than shading them itself.

Input

A DTextEntry whose Paint draws draw.RoundedBox(4, ...) in COL_PANEL_INPUT, then self:DrawTextEntryText(COL_STEEL_WHITE, COL_BRAND, COL_STEEL_WHITE). Overriding Paint drops the skin's placeholder, so draw one yourself in COL_TEXT_MUTED, inset Scale(4). DComboBox takes the same treatment.

List

  • Pure data: cityrp.ui.StyleList(list), before the list's first AddColumn or AddLine, because it wraps both. A DListView with header and row heights set from the table above, because the stock 17px row clips a scaled font.
  • Rows that hold controls: a DScrollPanel of hand-built rows, Dock(TOP), Scale(4) apart, draw.RoundedBox(4, ...).

Rows alternate COL_PANEL_CARD and COL_PANEL_ROW_ALT. The selected row is COL_PANEL_SELECTED.

Card

cityrp.ui.StyleCard(panel, fill), or cityrp.ui.PaintCard(panel, w, h, fill) from a Paint that has its own drawing to do on top.

A 1px inset border: draw.RoundedBox(4, ...) in COL_PANEL_HEADER, then the same box inset by 1 in the fill colour.

Tabs

Hand-built Dock(LEFT) buttons in a Scale(32) bar, top corners rounded. The active tab takes the content panel's fill so it merges into it, with a Scale(2) bar in COL_BRAND along its top edge. A tab bar that does not apply is collapsed, not shown disabled. Do not use DPropertySheet.

Confirmation

Anything the player cannot undo is confirmed first: destroy, sell, delete, remove from a clan. Reversible actions such as drop or unequip are not.

A confirmation is a small modal frame in the style above: a title, one or two lines of Body text saying what will happen, a Positive or Destructive button naming the action ("Destroy", not "Yes"), and a Neutral "Cancel" to its right. Escape cancels. Background blur is on. gamemode/core/derma/cl_makeleader.lua is the reference. Stock Derma_Query and Derma_StringRequest are for developer tools only.

Notification

A notification is a HUD chip: COL_UI_GREY, square, COL_HUD_OUTLINE, Saira Secondary, with a 2px bar along its top edge in the role colour for its type: generic is COL_BRAND, hint is COL_INFO, warning is COL_WARNING, error is COL_DANGER. It takes its position from the HUD's bounds.

This is the target for the notification renderer, which does not meet it yet. Panels keep calling notification.AddLegacy and need no change when the renderer is rebuilt.

Motion

Panels do not animate. HUD elements slide in over 0.5s and out over 0.25s, and fade over 0.4s.

Scale changes

A panel opened after the scale changes must be correct, and fonts must rebuild live through OnScaleChanged. Relayout of a panel that is already open is recommended: derive geometry in PerformLayout, or rebuild the panel with its state carried over.

Accessibility

  • Body text has a contrast ratio of at least 4.5:1 against the surface it sits on. Large or bold labels have at least 3:1. The palette above meets this when COL_TEXT_ON is used.
  • COL_TEXT_MUTED is not used on COL_PANEL_BUTTON.
  • State is never carried by colour alone. Pair a role colour with a label or an icon.
  • Everything follows the player's cl_llhud_scale setting through cityrp.ui.