Skip to content

RCSS Styling

RCSS is RmlUi’s stylesheet language. It is CSS with a smaller surface: the same selector syntax, the same cascade, the same box model, and a subset of the properties. This page covers what Lumina actually supports, which is narrower than CSS in some places and different in others.

Rules live in a .rcss file linked from a document head, or inline in a <style> block:

<link type="text/rcss" href="Theme.rcss"/>

RmlUi has no user-agent stylesheet. A browser ships default rules that make <div> a block, <p> a paragraph with margins, <h1> big and bold. RmlUi ships none of that. Every element starts as display: inline with no margin, no padding, and no font size of its own.

The visible symptom: a panel you gave a width, padding, and background-color renders as a thin smear of text that ignores all three, because an inline box does not take a width.

Start every stylesheet with this line:

div { display: block; box-sizing: border-box; }

RmlUi’s own debugger stylesheet does exactly this. Add whatever other container tags you use.

SelectorMatches
divEvery element with that tag.
.panelEvery element with that class.
#hudThe element with that id.
div.panelElements matching all parts.
a bb anywhere inside a.
a > bb that is a direct child of a.
a + bb immediately after sibling a.
a ~ bb after sibling a.
a, bEither.
*Everything.

Specificity and the cascade work as in CSS: more specific wins, later wins on a tie, and style="..." on the element beats any rule.

Pseudo-classState
:hoverThe cursor is over the element.
:activeThe element is being pressed.
:focusThe element has keyboard focus.
:checkedA checkbox, radio, option, or tab is selected.
:disabledA form control is disabled.

Structural selectors are also supported: :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, :nth-child(an+b), :nth-last-child(), :nth-of-type(), :nth-last-of-type(), :empty, and :not(selector).

.button:hover { background-color: #6c7086; }
.row:nth-child(2n) { background-color: #181825; }
.check:checked { background-color: #a6e3a1; }
UnitMeaning
dpDensity-independent pixel. Use this by default.
pxRaw pixels. Does not scale with the display.
%Percentage of the containing block.
em / remRelative to this element’s / the root’s font size.
vw / vhPercentage of the viewport width / height.
in, cm, mm, pt, pcPhysical units, scaled like dp.
deg / radAngles, for transform and gradients.

For screen-space UI the density ratio is the viewport height divided by 1080, clamped so it never goes below 1. So at 1080p and below, 1dp is 1px; at 1440p a dp-authored layout comes out proportionally larger instead of shrinking into the corner. Author in dp and your UI holds its proportions across resolutions.

px is right for hairlines and anything that should stay exactly one pixel.

display accepts none, block, inline, inline-block, flow-root, flex, inline-flex, and the table family. There is no CSS grid.

Flexbox is the full modern implementation, and it is what you want for most layout:

body {
width: 100%; height: 100%;
display: flex;
align-items: center; /* flex-start | flex-end | center | baseline | stretch */
justify-content: center; /* ... | space-between | space-around | space-evenly */
}
.toolbar {
display: flex;
flex-direction: row; /* row | row-reverse | column | column-reverse */
flex-wrap: wrap;
gap: 12dp; /* row-gap + column-gap */
}
.toolbar .spacer { flex-grow: 1; }

Positioning works as in CSS: static, relative, absolute, fixed, with top / right / bottom / left and z-index.

Also worth knowing: for a positioned child’s percentage offsets to resolve against its parent, the parent needs position: relative, same as CSS.

Everything in this list is registered and works.

GroupProperties
Boxwidth, height, min-width, max-width, min-height, max-height, margin (+ per-side), padding (+ per-side), box-sizing
Borderborder-width, border-color (+ per-side), border-radius (+ per-corner), the border / border-top / etc. shorthands
Layoutdisplay, position, top, right, bottom, left, float, clear, z-index, overflow, overflow-x, overflow-y, clip, visibility, vertical-align
Flexflex, flex-flow, flex-direction, flex-wrap, flex-grow, flex-shrink, flex-basis, align-items, align-self, align-content, justify-content, gap, row-gap, column-gap
Colorbackground-color, color, image-color, caret-color, opacity
Textfont-family, font-style, font-weight, font-size, line-height, letter-spacing, text-align, text-decoration, text-transform, white-space, word-break
Effectsdecorator, font-effect, transform, transform-origin, perspective, transition, animation
Interactioncursor, drag, tab-index, focus, pointer-events, nav-up / -right / -down / -left, scrollbar-margin, overscroll-behavior

Colors accept #rgb, #rrggbb, #rrggbbaa, rgb(), rgba(), and the standard color keywords.

These properties parse without error and then draw nothing. They need render-interface features (shader compilation and offscreen layers) that Lumina’s RmlUi renderer does not implement.

AvoidUse instead
box-shadowA border, or a ninepatch decorator with a baked shadow.
filter, backdrop-filterBake the effect into the texture.
mask-imageA pre-masked texture, or ninepatch.
linear-gradient, radial-gradient, conic-gradient, and their repeating- formshorizontal-gradient / vertical-gradient (below).
The shader decoratorAn image decorator.

A shader-based decorator that cannot compile logs [RmlUi] Could not generate decorator element data: every frame, so a flood of that message in the log means one of these slipped into a stylesheet.

decorator draws the background of an element. These are the ones that work:

DecoratorDraws
horizontal-gradient(start stop)A two-stop gradient, left to right.
vertical-gradient(start stop)A two-stop gradient, top to bottom.
image(path)A texture stretched over the element.
tiled-horizontal, tiled-vertical, tiled-boxA texture tiled with fixed caps.
ninepatch(outer, inner)A nine-slice texture that stretches without distorting corners.
.header {
decorator: vertical-gradient(#313244 #1e1e2e);
}
@decorator brand-fill : horizontal-gradient {
start-color: #cba6f7;
stop-color: #89b4fa;
}
.title { decorator: brand-fill; }

font-effect is rendered into the font atlas, so unlike box-shadow it works:

.hud-title {
font-effect: outline(2dp #000000);
}

Available effects are outline, glow, shadow, and blur.

Lumina registers the fonts, not your stylesheet. Two families are available:

FamilyWeightsUse for
LuminaRegularBody text. This is the engine default.
JetBrains Monobold, extra-bold onlyNumbers, timers, key caps.

The engine stamps font-family: Lumina on each context root, so omit font-family entirely and everything inherits a working font.

.stat-value {
font-family: JetBrains Mono;
font-weight: bold;
}

Both work and are CPU-side, so they are safe to use.

.button {
transition: background-color 0.15s ease-out;
}
@keyframes pop {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1.0); opacity: 1; }
}
.dialog { animation: 0.2s ease-out pop; }
At-rulePurpose
@decoratorDefine a named, reusable decorator.
@spritesheetName sub-rectangles of a texture, usable as sprite on <img> and in decorators.
@keyframesDefine an animation.
@mediaApply rules conditionally, for example by resolution.

@import and @font-face are not supported. Link stylesheets with <link>, and use the fonts the engine registers.

RmlUi’s layout is document-granular: changing one element’s text dirties the whole document’s layout, not just that element’s subtree. That is fine for a menu and matters for a HUD that updates every frame.

Three things that help:

  • Only push values when they change. A view-model’s Set helper already does this: it compares before pushing, so a health value that stays at 100 costs nothing. Formatting a string every frame and assigning it unconditionally defeats that, so compute first and assign second.
  • Prefer block layout for simple rows. A flex item with an auto main size makes RmlUi run a throwaway sub-layout just to measure it. A row with justify-content: space-between and two auto-sized children pays that twice per reflow. For a plain label-and-value row, float: left with an explicit width and a right-aligned value is cheaper.
  • Split volatile readouts into their own document. A per-frame counter in its own small document reflows only that document.

The renderer is not the bottleneck: geometry is cached and batched, and skipped entirely when nothing changed. The cost is CPU layout.