ui
Mod UI: placement components, toasts, theme, CSS colors, toolbar and floating panels, and map layer visibility.
api.ui. 26 members. Components registered under a placement are rendered by that placement's host component. The registry is cleared on game end (hosts re-render) and on reloadMods (hosts re-render on the next registration). The mod loader removes one mod's entries before re-executing its script and logs [Modding API] Unmounted <n> UI component(s) for mod <modId> (development builds only) when any were removed. The active mod id is set during the synchronous part of a mod script and of each hook callback registered while one was set; code after an await or in a timer runs with none.
| Member | Returns | Summary |
|---|---|---|
showNotification(message, type?, title?) | void | Show a toast. |
addButton(placement, options) | void | Add a button to a placement. |
addToggle(placement, options) | void | Add a checkbox with a label to a placement. |
addSlider(placement, options) | void | Add a range input with a label to a placement. |
addSelect(placement, options) | void | Add a <select> with a label to a placement. |
addText(placement, options) | void | Add a paragraph of text to a placement. |
addSeparator(placement, options) | void | Add a horizontal rule (<hr> with classes my-2 border-border) to a placement. |
addToolbarButton(options) | void | Add an icon button to the top-bar placement. |
addToolbarPanel(options) | void | Add a top-bar icon button that toggles a fixed panel anchored under the button. |
addFloatingPanel(options) | void | Add a top-bar icon button that toggles a draggable, resizable fixed panel. |
registerComponent(placement, component) | void | Register a React component under a placement. |
unregisterComponent(placement, componentId) | void | Remove one component from a placement by id. |
getComponents(placement) | UIComponent[] | The components registered under a placement. |
forceUpdate(placement?) | void | Re-render every subscribed placement host. |
setTheme(theme) | void | Set the app theme. |
getTheme() | 'light' or 'dark' or 'system' | The stored theme setting. |
getResolvedTheme() | 'light' or 'dark' | The theme in effect on the root element. |
setAccentColor(hsl) | void | Set the --accent CSS variable on the root element. |
setPrimaryColor(hsl) | void | Set the --primary CSS variable on the root element. |
setCSSVariable(name, value) | void | Set a CSS variable on the root element. |
resetColors() | void | Remove the inline --primary, --primary-foreground, --accent and --accent-foreground overrides from the root element. |
getAvailableLayers() | string[] | Every layer name in the game's layer config, including debug-only layers. |
getLayerVisibility(layerName) | boolean | Whether a map layer is visible. |
setLayerVisibility(layerName, visible) | void | Set one map layer's visibility in the store. |
getAllLayerVisibility() | Record<string, boolean> | The visibility of every map layer. |
setMultipleLayerVisibility(layers) | void | Set several map layers' visibility in one store write. |
Notifications#
showNotification#
api.ui.showNotification(message: string, type?: 'info' | 'success' | 'error' | 'warning', title?: string): voidShow a toast.
The toast title is title or Mod Notification. The body is message. Type error renders the destructive toast variant. The other types render the default variant. The toast is created with a 5000 ms duration. A new toast replaces the one on screen. The toast callback is registered when the game view mounts. Calls made before that (for example from the main menu before the first game of the session) are queued and flushed then.
| Param | Type | Default | Description |
|---|---|---|---|
message | string | required | toast body text. |
type | 'info' or 'success' or 'error' or 'warning' | none | info, success, error or warning; default info. |
title | string | none | toast title; default Mod Notification. |
Errors: thrown error: logs [Modding API] Error showing notification:, does nothing.
Side effects: pushes to the toast store; while queued, logs [Modding API] Notification queued (<type>): <message> (development builds only).
api.ui.showNotification('Line A reached 10,000 riders', 'success', 'Milestone');UI Primitives#
addButton#
api.ui.addButton(placement: UIPlacement, options: { id: string; label: string; icon?: string; onClick: () => void; }): voidAdd a button to a placement.
In main-menu, escape-menu and escape-menu-buttons the button renders as the Vignelli-style SubwayButton (size lg, reversed arrow, bearing 0) and icon is ignored. In every other placement it renders as a full-width <button> with an optional 14 px Lucide icon before the label. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | button definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
label | string | required | Button text. |
icon | string | none | Lucide icon name rendered at 14 px before the label; ignored in the three menu placements; an unknown name renders no icon. |
onClick | () => void | required | Click handler. |
Errors: thrown error: logs [Modding API] Error adding button:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added button "<id>" to <placement> (development builds only).
api.ui.addButton('escape-menu-buttons', {
id: 'my-mod-bonus',
label: 'Claim bonus',
icon: 'DollarSign',
onClick: () => api.actions.addMoney(1_000_000),
});addToggle#
api.ui.addToggle(placement: UIPlacement, options: { id: string; label: string; defaultValue?: boolean; onChange: (value: boolean) => void; }): voidAdd a checkbox with a label to a placement.
The checkbox keeps its own state and calls onChange with the new boolean on every change. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | toggle definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
label | string | required | Text after the checkbox. |
defaultValue | boolean | false | Initial checked state. |
onChange | (value: boolean) => void | required | Called with the new checked state on every change. |
Errors: thrown error: logs [Modding API] Error adding toggle:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added toggle "<id>" to <placement> (development builds only).
api.ui.addToggle('settings-menu', {
id: 'my-mod-night-mode',
label: 'Night economy',
defaultValue: true,
onChange: (enabled) => api.storage.set('nightMode', enabled),
});addSlider#
api.ui.addSlider(placement: UIPlacement, options: { id: string; label: string; min: number; max: number; step?: number; defaultValue?: number; onChange: (value: number) => void; }): voidAdd a range input with a label to a placement.
The label shows <label>: <value> and updates as the slider moves. onChange receives the numeric value on every input change. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | slider definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
label | string | required | Label text; rendered as <label>: <value>. |
min | number | required | Lower bound of the range input; also the initial value when defaultValue is omitted. |
max | number | required | Upper bound of the range input. |
step | number | 1 | Range input step. |
defaultValue | number | min | Initial value. |
onChange | (value: number) => void | required | Called with the numeric value on every input change. |
Errors: thrown error: logs [Modding API] Error adding slider:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added slider "<id>" to <placement> (development builds only).
api.ui.addSlider('settings-menu', {
id: 'my-mod-bubble-scale',
label: 'Demand bubble size',
min: 0.25,
max: 3,
step: 0.25,
defaultValue: 1,
onChange: (value) => api.actions.setDemandBubbleScale(value),
});addSelect#
api.ui.addSelect(placement: UIPlacement, options: { id: string; label: string; options: { value: string; label: string }[]; defaultValue?: string; onChange: (value: string) => void; }): voidAdd a <select> with a label to a placement.
The initial value is defaultValue, else the first option's value, else the empty string. onChange receives the selected value string on every change. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | select definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
label | string | required | Label text above the select. |
options | { value: string; label: string }[] | required | Option list; each entry renders <option value={value}>{label}</option>. |
defaultValue | string | none | Initial selected value; when omitted the first option's value is used, or the empty string for an empty list. |
onChange | (value: string) => void | required | Called with the selected value string on every change. |
Errors: thrown error: logs [Modding API] Error adding select:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added select "<id>" to <placement> (development builds only).
api.ui.addSelect('settings-menu', {
id: 'my-mod-speed',
label: 'Default speed',
options: [
{ value: 'normal', label: 'Normal' },
{ value: 'fast', label: 'Fast' },
{ value: 'ultrafast', label: 'Ultrafast' },
],
defaultValue: 'normal',
onChange: (speed) => api.actions.setSpeed(speed),
});addText#
api.ui.addText(placement: UIPlacement, options: { id: string; text: string; className?: string; }): voidAdd a paragraph of text to a placement.
Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | text definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
text | string | required | Paragraph content. |
className | string | text-sm text-muted-foreground | Class list on the <p>. |
Errors: thrown error: logs [Modding API] Error adding text:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added text "<id>" to <placement> (development builds only).
api.ui.addText('main-menu', { id: 'my-mod-version', text: 'My Mod v1.2 loaded', className: 'text-xs opacity-70' });addSeparator#
api.ui.addSeparator(placement: UIPlacement, options: { id: string; }): voidAdd a horizontal rule (<hr> with classes my-2 border-border) to a placement.
Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement id. |
options | object | required | separator definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within the placement; a repeat id replaces the earlier entry. |
Errors: thrown error: logs [Modding API] Error adding separator:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added separator "<id>" to <placement> (development builds only).
api.ui.addText('escape-menu', { id: 'my-mod-heading', text: 'My Mod' });
api.ui.addSeparator('escape-menu', { id: 'my-mod-sep' });
api.ui.addButton('escape-menu', { id: 'my-mod-reset', label: 'Reset colors', onClick: () => api.ui.resetColors() });Toolbar Buttons#
addToolbarButton#
api.ui.addToolbarButton(options: { id: string; icon: string; tooltip?: string; onClick: () => void; isActive?: () => boolean; }): voidAdd an icon button to the top-bar placement.
The button is a <div> with the game's toolbar button classes and the native title set to tooltip. It switches to the active style while isActive() returns true. When isActive is given the button re-renders every 500 ms to poll it. The registry entry goes under top-bar. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
options | object | required | button definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within top-bar; a repeat id replaces the earlier entry. |
icon | string | required | Lucide icon name; rendered at 20 px (w-5 h-5); an unknown name renders nothing. |
tooltip | string | "" | Native title attribute text. |
onClick | () => void | required | Click handler. |
isActive | () => boolean | none | Polled every 500 ms; true renders the active toolbar style; when omitted the button stays inactive. |
Errors:
- icon name not in Lucide: logs
[Modding API] Icon "<icon>" not foundat render (on every 500 ms poll whenisActiveis given), renders nothing. - thrown error: logs
[Modding API] Error adding toolbar button:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added toolbar button "<id>" (development builds only).
let overlayOn = false;
api.ui.addToolbarButton({
id: 'my-mod-overlay',
icon: 'Layers',
tooltip: 'Toggle road labels',
onClick: () => {
overlayOn = !overlayOn;
api.ui.setLayerVisibility('roadLabels', !overlayOn);
},
isActive: () => overlayOn,
});addToolbarPanel#
api.ui.addToolbarPanel(options: { id: string; icon: string; tooltip?: string; title?: string; width?: number; render: React.ComponentType; }): voidAdd a top-bar icon button that toggles a fixed panel anchored under the button.
The panel is portaled into the overlay root. Without one it renders in place. It opens below the button, right-aligned to it, with an 8 px gap and an 8 px minimum right margin to the viewport edge. Its max height is the viewport height below the panel top minus 8 px, 120 px minimum. The header shows title or id and a close button. Only the body scrolls. The button re-measures on window resize while the panel is open. The button's native title is tooltip. The registry entry goes under top-bar. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
options | object | required | panel definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within top-bar; a repeat id replaces the earlier entry; shown as the header when title is omitted. |
icon | string | required | Lucide icon name rendered at 20 px; an unknown name renders nothing. |
tooltip | string | none | Native title attribute on the button. |
title | string | id | Header text. |
width | number | 380 | Panel width; px; 0 falls back to the default. |
render | React.ComponentType | required | Component rendered in the panel body with no props. |
Errors:
- icon name not in Lucide: logs
[Modding API] Icon "<icon>" not foundat render, renders nothing. - thrown error: logs
[Modding API] Error adding toolbar panel:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Added toolbar panel "<id>" (development builds only).
const { React, components } = api.utils;
api.ui.addToolbarPanel({
id: 'my-mod-summary',
icon: 'Info',
tooltip: 'Network summary',
title: 'Network summary',
width: 280,
render: () =>
React.createElement('div', { className: 'p-2 space-y-1' },
React.createElement('div', null, `Stations: ${api.gameState.getStations().length}`),
React.createElement('div', null, `Routes: ${api.gameState.getRoutes({ includeTempRoutes: false }).length}`),
),
});Floating Panels#
addFloatingPanel#
api.ui.addFloatingPanel(options: {…}): voidAdd a top-bar icon button that toggles a draggable, resizable fixed panel.
The panel is portaled into the overlay root. Without one it renders in place. Position and size persist in localStorage key floating-panel-<id> as { x, y, width, height }, written on open and whenever they change while the panel is open. Without saved state the panel opens at x = max(50, viewportWidth - defaultWidth - 80) px, y = 60 px. Dragging the header clamps x to [0, viewportWidth - 100] px and y to [0, viewportHeight - 50] px. Eight resize handles (corners and edges) respect minWidth and minHeight. There is no upper bound. The header shows title or id, the current size, and a close button. render receives { width, height } where height is the panel height minus 50 px. The registry entry goes under top-bar. Registry rules as for registerComponent: stored by id under the placement (a second call with the same id replaces the first), tagged with the active mod id (untagged when none is active), every subscribed placement re-renders after the call, and the entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
options | object | required | panel definition. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Registry id within top-bar and the localStorage key suffix floating-panel-<id>; shown as the header when title is omitted. |
icon | string | required | Lucide icon name rendered at 20 px; an unknown name renders nothing. |
tooltip | string | none | Native title attribute on the button; when omitted, title is used, then the empty string. |
title | string | id | Header text. |
defaultWidth | number | 500 | Initial width when no saved state exists; px; 0 falls back to the default. |
defaultHeight | number | 400 | Initial height when no saved state exists; px; 0 falls back to the default. |
minWidth | number | 300 | Lower bound while resizing; px; 0 falls back to the default. |
minHeight | number | 200 | Lower bound while resizing; px; 0 falls back to the default. |
render | React.ComponentType<{ width: number; height: number }> | required | Component rendered in the panel body with { width, height }; height is the panel height minus 50 px. |
Errors:
- icon name not in Lucide: logs
[Modding API] Icon "<icon>" not foundat render, renders nothing. - thrown error: logs
[Modding API] Error adding floating panel:, does nothing.
Side effects: writes the UI component registry; localStorage floating-panel-<id>; logs [Modding API] Added floating panel "<id>" (development builds only).
api.ui.addFloatingPanel({
id: 'my-stats',
icon: 'BarChart3',
title: 'My Stats',
defaultWidth: 600,
defaultHeight: 400,
render: ({ width, height }) => React.createElement('div', { style: { width, height } }, '...'),
});
// render gets height 350 on first open (400 - 50).Custom React Components#
registerComponent#
api.ui.registerComponent(placement: UIPlacement, component: UIComponent): voidRegister a React component under a placement.
The component is stored by id. A second call with the same id under the same placement replaces the first. The entry's modId defaults to the active mod id (none when no mod id is active). Placement hosts render a function component with props. Hosts other than the settings screen also render a React element as is. Every subscribed placement re-renders after the call. The entry is removed before the owning mod re-executes on reload, on reloadMods, and on game end.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | target placement id. |
component | UIComponent | required | entry with id, component and optional props. |
Errors:
- placement, component or component.id falsy: logs
[Modding API] Invalid UI component - must have placement and component with id, does nothing. - component neither a function nor a React element: every host except the settings screen logs
[Modding API] Invalid component type for mod "<component id>":at render (the overlay host logs[Modding API] Invalid component type for overlay mod "<component id>":) and renders nothing. - thrown error: logs
[Modding API] Error registering UI component:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Registered UI component: <id> at <placement> (development builds only).
const { React } = api.utils;
function BudgetBadge({ label }) {
return React.createElement('span', { className: 'text-xs px-2' }, `${label}: ${Math.round(api.gameState.getBudget())}`);
}
api.ui.registerComponent('bottom-bar', { id: 'my-mod-budget', component: BudgetBadge, props: { label: 'Cash' } });unregisterComponent#
api.ui.unregisterComponent(placement: UIPlacement, componentId: string): voidRemove one component from a placement by id.
The placement entry is deleted when its last component is removed. Subscribed placements re-render when the placement has a registry entry, whether or not the id matched.
| Param | Type | Default | Description |
|---|---|---|---|
placement | UIPlacement | required | placement the component was registered under. |
componentId | string | required | the id given at registration. |
Errors:
- placement or componentId falsy: logs
[Modding API] Invalid unregister - must have placement and componentId, does nothing. - thrown error: logs
[Modding API] Error unregistering UI component:, does nothing.
Side effects: writes the UI component registry; logs [Modding API] Unregistering UI component: <id> from <placement> (development builds only) before looking the id up.
api.ui.addText('debug-panel', { id: 'my-mod-debug-note', text: 'My Mod loaded' });
setTimeout(() => api.ui.unregisterComponent('debug-panel', 'my-mod-debug-note'), 10_000);getComponents#
api.ui.getComponents(placement: UIPlacement): UIComponent[]The components registered under a placement.
unregisterComponent, every mod script execution, reloadMods and game end replace or drop the array, so a reference held across them goes stale.
Parameter placement (UIPlacement): placement id.
Returns: UIComponent[]. the live registry array for the placement; an empty array when none are registered or on error.
Errors: thrown error: logs [Modding API] Error getting UI components:, returns [].
const mine = api.ui.getComponents('settings-menu').filter((c) => c.id.startsWith('my-mod-'));
console.log(`${mine.length} settings rows registered`);forceUpdate#
api.ui.forceUpdate(placement?: UIPlacement): voidRe-render every subscribed placement host.
The placement argument changes only the log line. All subscribers re-render.
Parameter placement (UIPlacement): placement id written into the log line; default none (logs (all)).
Errors: thrown error: logs [Modding API] Error forcing UI update:, does nothing.
Side effects: logs [Modding API] Force UI update for <placement> or [Modding API] Force UI update (all).
const { React } = api.utils;
let riders = 0;
api.ui.registerComponent('bottom-bar', {
id: 'my-mod-riders',
component: () => React.createElement('span', null, `${riders} riders/h`),
});
api.hooks.onHourChange(() => {
riders = api.gameState.getRidershipStats().totalRidersPerHour;
api.ui.forceUpdate('bottom-bar');
});Theme Control#
setTheme#
api.ui.setTheme(theme: 'light' | 'dark' | 'system'): voidSet the app theme.
The value is written to localStorage key vite-ui-theme and a theme-change window event carries it to the theme provider. The provider applies the light or dark class on the root element. system follows the OS prefers-color-scheme query.
Parameter theme ('light' | 'dark' | 'system'): light, dark or system.
Errors: thrown error: logs [Modding API] Error setting theme:, does nothing.
Side effects: localStorage vite-ui-theme; window event theme-change; logs [Modding API] Theme set to: <theme> (development builds only).
api.ui.setTheme('dark'); // 'light', 'dark' or 'system'getTheme#
api.ui.getTheme(): 'light' | 'dark' | 'system'The stored theme setting.
Returns: 'light' | 'dark' | 'system'. the localStorage vite-ui-theme value; system when unset or on error.
Errors: thrown error: logs [Modding API] Error getting theme:, returns system.
if (api.ui.getTheme() === 'system') console.log('Following the OS theme');getResolvedTheme#
api.ui.getResolvedTheme(): 'light' | 'dark'The theme in effect on the root element.
Returns: 'light' | 'dark'. dark when the root element has the dark class, otherwise light; light on error.
Errors: thrown error: logs [Modding API] Error getting resolved theme:, returns light.
// the theme actually shown, with 'system' resolved
const colors = api.gameState.getMapColors(api.ui.getResolvedTheme() === 'dark');Color Customization#
setAccentColor#
api.ui.setAccentColor(hsl: string): voidSet the --accent CSS variable on the root element.
The value is the space-separated HSL triple <hue> <saturation>% <lightness>% with no hsl() wrapper. The theme consumes it as hsl(var(--accent)). Light-theme default 0 0% 96.1%, dark-theme default 0 0% 14.9%. The inline value overrides both. --accent-foreground is set to 0 0% 98% in the same call.
Parameter hsl (string): <hue> <saturation>% <lightness>%, for example 210 100% 50%.
Errors: thrown error: logs [Modding API] Error setting accent color:, does nothing.
Side effects: inline style on the root element; logs [Modding API] Accent color set to: <hsl> (development builds only).
api.ui.setAccentColor('210 100% 50%'); // HSL triple, no hsl() wrappersetPrimaryColor#
api.ui.setPrimaryColor(hsl: string): voidSet the --primary CSS variable on the root element.
The value is the space-separated HSL triple <hue> <saturation>% <lightness>% with no hsl() wrapper. The theme consumes it as hsl(var(--primary)). Light-theme default 0 0% 9%, dark-theme default 0 0% 98%. The inline value overrides both. --primary-foreground is set to 0 0% 98% in the same call.
Parameter hsl (string): <hue> <saturation>% <lightness>%, for example 210 100% 50%.
Errors: thrown error: logs [Modding API] Error setting primary color:, does nothing.
Side effects: inline style on the root element; logs [Modding API] Primary color set to: <hsl> (development builds only).
api.ui.setPrimaryColor('142 71% 45%');setCSSVariable#
api.ui.setCSSVariable(name: string, value: string): voidSet a CSS variable on the root element.
A name without the -- prefix gets it added. resetColors does not remove variables set here unless the name is one of its four.
| Param | Type | Default | Description |
|---|---|---|---|
name | string | required | variable name with or without --. |
value | string | required | CSS value string. |
Errors: thrown error: logs [Modding API] Error setting CSS variable:, does nothing.
Side effects: inline style on the root element; logs [Modding API] CSS variable <name with the -- prefix> set to: <value> (development builds only).
api.ui.setCSSVariable('--my-mod-highlight', '48 96% 53%');
api.ui.setCSSVariable('radius', '0.25rem'); // the -- prefix is addedresetColors#
api.ui.resetColors(): voidRemove the inline --primary, --primary-foreground, --accent and --accent-foreground overrides from the root element.
Other variables set with setCSSVariable stay.
Errors: thrown error: logs [Modding API] Error resetting colors:, does nothing.
Side effects: inline style on the root element; logs [Modding API] Color overrides reset (development builds only).
api.ui.setAccentColor('0 84% 60%');
api.ui.resetColors(); // back to the theme defaultsLayer Visibility#
getAvailableLayers#
api.ui.getAvailableLayers(): string[]Every layer name in the game's layer config, including debug-only layers.
Returns: string[]. a new array of the layer config keys: tracks, trackSpeeds, trackCant, trackElevations, trackTypes, trackTph, trackOwners, trains, trainsRouteColor, trainWarnings, trainWarningExtras, routes, routeLoad, tracksBase, trackGraph, trackArrows, bearingMarkers, signalPoints, signalLines, nodes, centerlines, stationDots, stationLabels, yards, yardLabels, popMovements, cameraCenter, stationPlatforms, buildings, buildingFoundations, oceanFoundations, roads, roadLabels, bridges, neighborhoodLabels, commercialAreas, debugOffsetLines, laneWeldAreas, annotations, gradeCrossings, railCrossings; [] on error.
Errors: thrown error: logs [Modding API] Error getting available layers:, returns [].
const layers = api.ui.getAvailableLayers();
console.log(layers.includes('trackSpeeds'));getLayerVisibility#
api.ui.getLayerVisibility(layerName: string): booleanWhether a map layer is visible.
Parameter layerName (string): a name from getAvailableLayers.
Returns: boolean. the store value; false for an unknown name or on error.
Errors: thrown error: logs [Modding API] Error getting layer visibility:, returns false.
if (api.ui.getLayerVisibility('trains')) console.log('Trains are shown');setLayerVisibility#
api.ui.setLayerVisibility(layerName: string, visible: boolean): voidSet one map layer's visibility in the store.
The store receives a new layers object with the single key changed.
| Param | Type | Default | Description |
|---|---|---|---|
layerName | string | required | a name from getAvailableLayers. |
visible | boolean | required | new visibility. |
Errors:
- unknown name: logs
[Modding API] Unknown layer: <name>. Use getAvailableLayers() to see valid options., does nothing. - thrown error: logs
[Modding API] Error setting layer visibility:, does nothing.
Side effects: store layersToShow; logs [Modding API] Set layer "<name>" visibility to <visible> (development builds only).
api.ui.setLayerVisibility('buildingFoundations', false);getAllLayerVisibility#
api.ui.getAllLayerVisibility(): Record<string, boolean>The visibility of every map layer.
Returns: Record<string, boolean>. a shallow copy of the store's layersToShow keyed by layer name; {} on error.
Errors: thrown error: logs [Modding API] Error getting all layer visibility:, returns {}.
const hidden = Object.entries(api.ui.getAllLayerVisibility()).filter(([, on]) => !on).map(([name]) => name);
console.log(`Hidden layers: ${hidden.join(', ')}`);setMultipleLayerVisibility#
api.ui.setMultipleLayerVisibility(layers: Record<string, boolean>): voidSet several map layers' visibility in one store write.
Unknown names are dropped and the valid entries are still applied.
Parameter layers (Record<string, boolean>): map of layer name to visibility.
Errors:
- unknown names present: logs
[Modding API] Unknown layers ignored: <a, b>as a warning, applies the rest. - thrown error: logs
[Modding API] Error setting multiple layer visibility:, does nothing.
Side effects: store layersToShow; logs [Modding API] Updated visibility for <n> layers (development builds only).
// a clean screenshot view
api.ui.setMultipleLayerVisibility({ trains: false, stationLabels: false, roadLabels: false });UIPlacement#
Placement ids and where each renders.
settings-menu: a searchable row inside the settings screen (the home Settings page and the pause menu Settings view), one per component. The screen reads the registry on its own renders and does not subscribe to changes. escape-menu-buttons: appended to the end of the pause menu's second button grid (the grid that starts with Settings). escape-menu: a separate scrollable grid between the pause menu's button grids and its exit buttons (max height 128 px, two columns from 768 px wide), shown only when a component is registered. main-menu: a scrollable grid between the home menu buttons and the exit button (max height 192 px, two columns from 768 px wide), shown only when a component is registered. bottom-bar: a horizontally scrollable strip at the end of the in-game bottom bar, at most 40% of its width, shown only when a component is registered. top-bar: inline in the right-hand toolbar icon row. The inline limit is Number.MAX_SAFE_INTEGER, so every entry renders inline. debug-panel: the debug info panel, rendered only while that panel is open. Each entry gets its own padded box. overlay: a full-viewport fixed inset-0 z-50 pointer-events-none layer portaled to document.body, mounted in the game view only. Children opt into pointer events themselves. Hosts other than the settings screen subscribe to registry changes and re-render on every registration.
type UIPlacement = | 'settings-menu' | 'escape-menu' | 'escape-menu-buttons' | 'main-menu' | 'bottom-bar' | 'top-bar' | 'debug-panel' | 'overlay'UIComponent#
A registry entry for a placement.
Hosts render component with props when it is a function component. Hosts other than the settings screen also render a React element as is.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique id within the placement; a repeat id replaces the earlier entry. |
component | React.ComponentType<any> | required | Function component rendered with props. See UIComponent.component below. |
props | Record<string, any> | {} | Props passed to a function component. |
modId | string | none | Owning mod id; when omitted, the active mod id at registration. Mod reloads remove entries by this id. |
UIComponent.component#
Function component rendered with props. A React element is rendered as is by every host except the settings screen. Any other value makes those hosts log [Modding API] Invalid component type for mod "<component id>": (the overlay host: [Modding API] Invalid component type for overlay mod "<component id>":) at render and render nothing.