Styles
Railroader’s style system enables modders to customize visual aspects of rolling stock: colors, text, decal images, visibility, and so forth, without having to modify the car definitions themselves or create duplicate definitions. Styles are used to drive the appearances of practically all cars in the game. The system also enables the player to individually set custom colors, text, and more using the Customize window.
As a mod author you can enable your equipment mods to participate in the built-in styles (such as for freight and passenger equipment) by adopting the same component style classes the base game’s cars use — chiefly a body renderer group and the standard decal contents.
Styles are defined in mods and are automatically applied to cars arriving at interchange based on filter parameters (reporting mark and car type), or manually applied to your own cars via the Customize window or the /style console command.
What’s in a Style
Section titled “What’s in a Style”A style is composed of a series of entries, each with a:
- Selector: which components are being styled (for example, all number board decals)
- Property: which aspect of those components is being controlled (for example, the color)
- Value: the value being applied to the property (for example, white)
Selectors have a specific syntax. They must contain at least one of the following:
- Component type (e.g.,
Decal) - Component id — the unique identifier for this component
- Component style class — a component can be part of one or more classes; by specifying a class we select all components in that class
The selector is a string; the parts above are combined with a specific syntax, usually in one of these forms:
ComponentType#componentIdComponentType.componentStyleClass#componentId.componentStyleClass
To follow the example above, we could set the color of the left lettering decal to white with the following:
Decal#left-lettering(the selector, assuming the id of the decal isleft-lettering)color(the property)ffffff(hex color white)
This works, but we probably want the lettering on both sides to have the same appearance. If we give all the lettering decals the same style class in the car definition — a property on the Decal component — we can target both at once:
Decal.letteringcolorffffff
We’ve split those parts out to show them individually, but an actual style is defined with nested dictionaries:
"styles": { "Decal.lettering": { "color": "ffffff", "font-size": 10 }, "Renderer.grabIrons": { "color": "000000" }}If you’ve done any web development this will probably look familiar to you: it’s inspired by Cascading Style Sheets (CSS).
How Styling Works
Section titled “How Styling Works”When the components that use styles are built at runtime (such as when a car’s model is loaded), they ask the system for the values of specific style properties. For example, a Decal might ask what color its text should be. The answer is resolved by matching that component’s type, id, and class against three sources of styles, read in this order:
- The car definition’s own
stylesmap — defaults baked into the model itself. - The style assigned to the car, if any. The Customize window describes this as “Paint Scheme”.
- Inline overrides on that individual car, from the Customize window or
/style set.
All three cascade together as one sheet: specificity decides, and the order above only breaks a tie between rules aimed equally narrowly. So a style’s broad Renderer rule does not override a car definition’s Renderer.smokebox, while a style’s Renderer.body does beat a definition’s Renderer.body.
Each source contributes only the properties it actually names, so a player recoloring one decal keeps the rest of the style underneath. If nothing specifies a property, a default value is used.
This has an added benefit: cars that agree on component classes can share a style. That’s how the base game colors foreign road cars spawning at interchange, and how one set of passenger car styles covers many cars without configuring each one individually.
A car only needs the classes that make sense for it — give a component the class that describes what it is, and leave out the rest. A style naming a class the car doesn’t have has nothing to apply there, so a boxcar with no roof renderer group is styled perfectly well by a style that mentions one.
Defining a Style
Section titled “Defining a Style”To define a new style, add a CarStyle object to your asset pack’s Definitions.json:
{ "identifier": "myhandle/style-sou-green", "metadata": { "name": "Southern Railway Green" }, "definition": { "kind": "CarStyle", "name": "Southern Railway Green", "reportingMark": "SOU,SR", "carTypeFilter": "PB*", "styles": { "Renderer.body": { "color": "1B3D2F" }, "Renderer.trim": { "color": "D4AF37" }, "Decal": { "color": "D4AF37" }, "Decal.railroadName": { "text": "SOUTHERN" } } }}| Field | Required | Description |
|---|---|---|
kind | Yes | Must be "CarStyle" |
name | Yes | Display name shown in the Customize window |
reportingMark | No | Comma-separated reporting marks for auto-selection, or "*" for wildcard |
carTypeFilter | No | Car type filter restricting which cars can use this style. When absent, available to all car types |
carIdentifiers | No | Restricts this style to specific car definition identifiers (e.g. ["myhandle/gp7"]), bypassing carTypeFilter entirely. Use this when a style depends on car-specific geometry (like a herald control texture sized for one model) rather than being paintable onto any car of a given type |
carSeries | No | Road number series this style keeps, keyed by reporting mark — see Car Spawn Identity |
styles | Yes | Selector-to-properties map — see Selectors and Properties |
Selectors and Properties
Section titled “Selectors and Properties”Style selectors follow a CSS-like pattern with three dimensions: element type, style class, and an id.
Element Types
Section titled “Element Types”Renderer— Colorizable material groups (body paint, trim, etc.). These correspond toRendererGroupcomponents.Decal— Text and image decals (lettering, road numbers, data stencils, heralds). These correspond toDecalcomponents.
Selector Syntax
Section titled “Selector Syntax”The full set of forms, and what each matches:
| Selector | Example | Matches |
|---|---|---|
| Element only | "Renderer" | All Renderer components |
| Class only | ".body" | Any component (Renderer or Decal) with class body |
| Element + class | "Renderer.body" | Only Renderer components with class body |
| Several classes | ".body.roof" | Components carrying both classes |
| Id only | "#left-number-board" | The single Decal component whose definition key is left-number-board |
| Element + id | "Decal#left-number-board" | Same as above, scoped to Decal (ids are only meaningful for decals) |
| Class + id | ".roadNumber#left-number-board" | That component, and only if it carries class roadNumber |
| Everything | "Decal.roadNumber#left-number-board" | All three parts must match |
Writing several classes together is an and: ".cab.trim" matches only a component that carries
cab and trim, not either one. Their order does not matter — ".trim.cab" is the same
selector. This is how a modifier class earns its keep: a component classed "cab, trim"
is painted by a generic ".cab" rule, and a style that wants the trim a different
color targets ".cab.trim" without needing a class name of its own.
How Car Components Map to Selectors
Section titled “How Car Components Map to Selectors”A component only participates in style matching if it has the right shape:
-
RendererGroupcomponents match by theirstyleClassfield (e.g."body","trim") — there’s no id-based selector for renderer groups, since a car typically has only a few, each a distinct paintable zone. -
Decalcomponents match by both a style class and an id:- The class comes from the decal’s
styleClassfield if set. Otherwise it defaults to a class derived from the decal’scontentvalue: the five built-in values map toroadNumber,reportingMark,railroadName,data, andimage, and any custom value is used as the class as-is. - The id is the decal’s own key in the definition’s
componentsmap — e.g. a component keyed"left-number-board"also matches#left-number-boardandDecal#left-number-board.
Selectors match case-insensitively, in every part — element, class, and id alike.
Decal.RoadNumber,decal.roadnumber, andDecal.roadNumberare the same selector. Property names inside a rule (color,text,uvRect) are still matched exactly. - The class comes from the decal’s
Prefer an explicit styleClass over the id form as your styling point. Where decals need styling independently — cab and side lettering, say — give each one the class for the role it plays plus a modifier for where it sits, and target the pair as Decal.lettering.cab, rather than reaching for #left-cab-lettering:
| Decal Id | Style Class |
|---|---|
left-cab-lettering | lettering, roadNumber, cab |
right-cab-lettering | lettering, roadNumber, cab |
left-side-lettering | lettering, roadNumber, side |
right-side-lettering | lettering, roadNumber, side |
A class names the role a part plays, so it reads for itself, and any number of components filling that role can share it. It also doesn’t depend on whatever key the definition happens to use, so it keeps working if a decal is ever renamed or reorganized.
Listing several classes, as the rows above do, makes the component a member of each. It answers to a rule naming any one of them, so .lettering paints all four decals while Decal.lettering.cab reaches only the two on the cab — no cabLettering class needed to tell them apart. Where two rules match at the same specificity, the one naming the class listed later on the component wins.
Order the list from the broadest name to the narrowest: the region first, then the finer class, then where it sits. See Standard Classes for the names to use.
One Style, One Car
Section titled “One Style, One Car”Sometimes you want a specific result on a specific model: a herald whose control texture is cut for that car’s geometry, or a set of offsets that only make sense against one body. Give the car its own purpose-named style classes for those parts, and pin the style to the model with carIdentifiers so it’s never offered for a car it can’t paint properly. Reach for an id selector only for the one-off tweak that doesn’t justify a class of its own.
Pinning only controls which cars your style can be applied to. It doesn’t stop a general-purpose style matching the same reporting mark from being offered alongside it. To make yours the only ones offered, set requiresDedicatedStyles on the car definition.
Naming Rules
Section titled “Naming Rules”Style classes and component ids can only include specific characters, since a selector has to be able to say both. See Names Within a Definition.
Specificity
Section titled “Specificity”When multiple selectors match, properties merge in specificity order (lowest to highest):
- Element only (
"Renderer") - Class only (
".body") - Element + class (
"Renderer.body") - Two classes (
".body.roof"), then three, and so on - Id only (
"#left-number-board") - Element + id (
"Decal#left-number-board") - Class + id (
".roadNumber#left-number-board"), and again more classes beat fewer - Element + class + id (
"Decal.roadNumber#left-number-board")
The rule behind the list: an id outranks any number of classes, which outrank an element type, and between two rules with the same count the one naming more classes wins. When two rules are equally specific, the one matching the class listed later on the component wins.
Higher-specificity selectors override lower ones per-property. Set defaults on an element selector, then override for individual style classes:
"styles": { "Renderer": { "color": "2D5A27" }, "Renderer.trim": { "color": "D4AF37" }, "Decal": { "color": "FFFFFF" }, "Decal.railroadName": { "text": "SOUTHERN", "color": "D4AF37" }, "Decal.numberBoard.front": { "color": "metallic(#D4AF37, 0.8)" }}All renderers default to green, but trim gets gold. All decals default to white, but the railroad name gets gold text, and the front number board gets a shiny gold-metal finish.
Id selectors sit above class in specificity, for the rare case where two decals share a class but one needs a one-off tweak without splitting off a whole new class:
"Decal.numberBoard": { "color": "D4AF37" },"Decal#left-number-board": { "offset": "0.02 0" }Properties
Section titled “Properties”| Property | Applies to | Description |
|---|---|---|
color | Renderer, Decal | Zone/decal color — a hex string, or a paint-material function (see below) |
offset | Renderer, Decal | Positional nudge — "x y [z]" in meters |
offset-space | Renderer, Decal | Coordinate frame for offset: "local" (default, the component’s own surface-aligned frame) or "parent" (the car’s frame) |
enabled | Any component | Whether the component is present on the car (boolean) — false removes it, true brings it back. If no rule sets enabled for a component, the style leaves that component as it is rather than enabling it. A style and a Relay script should not both drive the same component’s enabled! |
Decals have more properties, for text and images. See Decal.
Those are the properties the game resolves itself. A style rule is an open key/value map, so it can carry any property name, and a Relay script can read whatever it likes out of one with car.resolveStyle(). Paint Maps work this way: their color1–color3 properties are defined and applied by the shared paint-map scripts rather than by the game, and those scripts read image as the decal’s control map rather than as a plain texture.
Color and the paint-material DSL: a bare hex string ("1B3D2F", with or without #) is a flat, matte color. Decals additionally understand two function forms for a metallic finish:
"metallic(#hex, m)"— metallic amountm(0–1) with a shiny default smoothness"metallic(#hex, m, s)"— metallic amountmplus an explicit smoothnesss(0–1)"smooth(#hex, s)"— non-metallic, with explicit smoothnesss(shorthand formetallic(#hex, 0, s))
The metallic amount m is a finish: 0 is flat enamel, 0.5 is metallic flake paint, and 1 is bare polished metal — brass, chrome, a bell — with no sparkle at all.
A decal left with no color at all gets a sensible fallback: if a named style is applied, its lettering contrasts against the Renderer.body color (light text on a dark body, or vice versa); otherwise it uses a neutral default.
The Renderer element’s built-in automatic colorizer reads color in all three forms — see Colorable Components. A bare hex sets the base color and leaves the material’s authored finish alone; metallic()/smooth() additionally override the finish, which takes effect on materials using one of the car paint shaders. Decals support the full DSL natively, no scripting required.
Enabling Customization
Section titled “Enabling Customization”A car’s Customize window shows options for customizing its appearance. A style can be chosen there as well, and specific properties can be further customized by the player.
By default, styles are not exposed to the player for customization. Instead, properties must be opted in by means of a customize-<property> key whose value is the label to show:
"styles": { "Renderer.jacketing": { "color": "1B2F17", "customize-color": "Jacketing" }, "Decal.lettering": { "color": "metallic(#CBA45A, 1.0)", "customize-color": "Lettering" }, "Decal.railroadName": { "customize-text": "Railroad Name" }, "Renderer.smokebox": { "color": "2B2B2B" }}In this example the jacketing, lettering and railroad name are editable and the smokebox is not.
Note that the property is only shown as editable when the car actually has that part. A shared style naming Renderer.body gives a boxcar its Body control and leaves it off a steam locomotive, whose renderer groups are classed jacketing, cab and the like. Likewise customize-text is shown only on decals with content types Text and RailroadName.
These keys work in a CarStyleDefinition and in a car definition’s own styles.
Customization can also be enabled from Relay, with ui.registerStyleProperty() — which additionally offers a slider, and can name a property no rule sets.
Standard Classes
Section titled “Standard Classes”Railroader ships a standard vocabulary of classes that the base game’s cars and styles use. Using it is optional, but we think it keeps packs and styles readable against each other, and it lets the same style work across multiple cars. A freight car classed body and lettering takes the built-in generic styles without needing one of its own, and a style you write against these names applies to anyone else’s car that follows the same convention.
Of course, sufficiently detailed models will have classes of their own.
All Rolling Stock
Section titled “All Rolling Stock”| Renderer Class | Used for |
|---|---|
body | Primary carbody: shell, hood, tender tank. Every car with a painted body has one |
roof | A roof the model paints separately. Stands alone, never body, roof — whether a roof matches the body is the style’s decision |
truck | Truck sideframes, on Truck definitions only. A car whose trucks follow its frame targets Renderer.truck beside its frame rule |
Every decal already answers the class matching its content — roadNumber, reportingMark, railroadName, data, or image. Put lettering in front of that class to make the car’s painted words and numbers reachable as one region:
{ "kind": "Decal", "content": "RoadNumber", "styleClass": "lettering, roadNumber" }Decal.lettering then letters the whole car in one rule, while Decal.roadNumber still picks the number out of it.
lettering means paint. A decal that renders a physical object — a lit number board, a cast brass plate, a herald — stays outside it, so a style’s lettering color can’t reach it. The test is what the decal sits on, not what the component is named.
The classes below are locomotive and tender vocabulary. No freight or passenger car in the base game declares any of them.
Freight Cars
Section titled “Freight Cars”body on the carbody, roof where the model has one, and lettering on every stencil. Freight lettering is nearly all Data content — capacity, light weight, dimensional stencils — which is still lettering:
{ "kind": "RendererGroup", "styleClass": "body" },{ "kind": "Decal", "content": "Data", "styleClass": "lettering, data" }A generic interchange style then paints the car with two rules:
"Renderer.body": { "color": "6A1B0B" },"Decal.lettering": { "color": "FFFFFF" }Cabooses (NE) style as freight.
Passenger Cars
Section titled “Passenger Cars”The same two classes. Every passenger decal is a road number or a railroad name, and styles letter both in one color.
Locomotives
Section titled “Locomotives”| Class | Used for |
|---|---|
cab | Cab exterior. Cab trim is cab, trim; a cab roof carries roof alone |
underframe | Frame, sills, underbody equipment |
pilot | Pilot, plow, coupler shroud. Sub-parts ride on it: pilot, beam, pilot, bumper, pilot, belt |
handrails | Handrails and railings |
grabIrons | Grab irons. Its own region, not a kind of handrails — a model declaring both colors them apart |
piping | Exposed piping and conduit |
interior | Cab interior |
numberBoard | Number board housing. The lit numerals on it are a decal classed bare numberBoard, with no content class, so a lettering color leaves them alone |
Where a style colors parts apart by position, a placement token follows the region: grabIrons, nose, grabIrons, rear, lettering, roadNumber, cab. front, side, left, and right are reserved for the same purpose but unused today.
Steam Locomotives
Section titled “Steam Locomotives”Steam locomotives have no body — a generic style paints them with a bare Renderer rule and overrides.
| Class | Used for |
|---|---|
jacketing | Boiler jacketing |
smokebox | Smokebox and front |
domes | Steam and sand domes. A number on one is lettering, roadNumber, dome |
cylinders | Cylinder block. Caps are cylinders, caps |
drivers | Drivers and wheel centers |
tires | Wheel tires |
appliances | Pumps, generators, boiler fittings |
runningBoard | Running boards and walkways |
headlight | Headlight casing |
bellMount | Bell and bell mount |
axleEnds | Axle ends and hub caps |
spotPlate | The painted inset behind the smokebox number plate. Its numerals are a decal classed roadNumber, spotPlate — cast brass, not lettering, so the car colors them itself |
Tenders
Section titled “Tenders”Tenders are Car definitions: body for the tank sides, body, deck for the deck, body, coalBoard for the coal boards, plus handrails, runningBoard, and truck as on the locomotive.
Your Own Classes
Section titled “Your Own Classes”A second class must name a kind of the first. cab, trim and body, deck hold; body, roof doesn’t, because a roof is a separate assembly rather than a kind of body.
For classes of your own — exhausts, sideSkirts, fuelTank — use lowerCamelCase, plural only for a set of like parts (domes, drivers), and don’t reuse a standard name for something else. A group classed body must be the carbody, or generic styles will paint the wrong thing.
Base Styles on a Car Definition
Section titled “Base Styles on a Car Definition”A Car definition can also carry its own built-in styles map — the same selector-to-properties shape as a CarStyleDefinition, but baked directly into the car rather than selectable as a separate style:
{ "kind": "Car", "carType": "XM", "styles": { "Decal.dataStencil": { "font-size": 8 } }}This is the lowest layer of the style cascade. It’s useful for defaults that should hold regardless of style, like a decal color for one car model.
Prop Styles
Section titled “Prop Styles”Prop definitions take styles too. A prop declares RendererGroup components and a base styles map with the same selectors as a car (Renderer.siding). A DetailModel on the prop is recolored with it.
A placement in map data can override these with its own styles map, which sits on top of the definition’s styles in the cascade. Props have no style definitions or player overrides.
A customize-color key in the definition’s styles adds a row to the map editor’s Colors section. Styles apply when the prop loads, and live while you edit in the map editor. They can’t be changed at runtime or from scripts.
The Message Log reports placement selectors that match nothing on the prop and entries that aren’t a property map, under Prop Styles. It also reports a RendererGroup whose material isn’t in the model’s MaterialMap.
The /style Command
Section titled “The /style Command”/style inspects and tweaks a car’s live style overrides from the in-game console — useful for dialling in a look before baking it into a style definition.
/style get <car> [<selector> <property>]/style set <car> <selector> <property> <value>/style export <car> <filename>/style export-full <car> <filename>/style import <car> <filename>getwith no selector/property dumps the car’s whole style KV value; with both, it shows the resolved value plus the raw inline override (if any).setwrites an inline override — the same kind of change the Customize window makes.valueofnullclears it.exportwrites a.rrstylevariant: a reference to the car’s base style plus its inline overrides.export-fullwrites a snapshot: the fully resolved selector→property map with no base reference, flattening everything into one self-contained style. Both save toDocuments/Railroader/Styles/<filename>.rrstyle.importapplies a.rrstylefile to a car, dropping any selectors the target car doesn’t actually have (e.g. importing a style authored for a different decal layout).
/style set MYRR-1234 Renderer.body color "metallic(#1B3D2F, 0.6)"/style set MYRR-1234 Decal.numberBoard.front offset "0.1 -0.05"/style get MYRR-1234 Renderer.body color/style export MYRR-1234 my-green-style/style import MYRR-5678 my-green-styleAuto-Selection
Section titled “Auto-Selection”A car that arrives without a style is given one automatically. This covers cars whose reporting mark is already settled before their paint is — one the player buys, a catalog photo, a car loaded from a save written before the style system. Foreign traffic spawning at interchange does not come through here: its mark and style are chosen together, described under Car Spawn Identity.
The pick is made from the styles the car is eligible for, preferring those whose reportingMark lists the mark it already carries, and is stable for a given car. A car no style suits keeps its definition’s own appearance rather than borrowing one written for another road. Setting requiresDedicatedStyles on the car narrows the pick to the styles that name it.
reportingMark only steers this automatic pick — it never stops a player choosing the style by hand in the Customize window.
In-Game Customization
Section titled “In-Game Customization”Players can change styles on owned cars using the Customize window via the Car inspector:
- Paint Scheme dropdown to select a style
- Per-property color and text overrides, one per control a style rule or an asset pack asked for
- Copy to Coupled applies the style to all coupled cars owned by the player
Every control is declared. A style rule asks for one with a customize-<property> key; an asset pack can also register one from Relay — with a slider, or for a property no rule sets — via ui.registerStyleProperty().
The style Property Object
Section titled “The style Property Object”Each car’s active style is stored in its properties under the style key. It has two sub-keys:
id— The identifier of the base style definition (e.g."myhandle/style-sou-green")styles— Optional inline overrides from player customization or/style set, using the same selector-to-properties format as the definition
These inline styles are the top layer of the cascade, so a player override (like changing a lettering color) rides on top of the style rather than being folded into it. If the style can no longer be resolved — its asset pack was removed or disabled — the id and the inline overrides both stop applying; the car falls back to its definition’s base styles.
{ "id": "myhandle/style-sou-green", "styles": { "Decal.railroadName": { "text": "MY RAILROAD" } }}You can read or write the style via the car’s properties using the "style" key in Relay scripts — see Car → Styling for getComponentsForStyle()/resolveStyle() — as well as using the /style and /set console commands.
© 2026 Giraffe Lab LLC. All rights reserved.