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.

MemberReturnsSummary
showNotification(message, type?, title?)voidShow a toast.
addButton(placement, options)voidAdd a button to a placement.
addToggle(placement, options)voidAdd a checkbox with a label to a placement.
addSlider(placement, options)voidAdd a range input with a label to a placement.
addSelect(placement, options)voidAdd a <select> with a label to a placement.
addText(placement, options)voidAdd a paragraph of text to a placement.
addSeparator(placement, options)voidAdd a horizontal rule (<hr> with classes my-2 border-border) to a placement.
addToolbarButton(options)voidAdd an icon button to the top-bar placement.
addToolbarPanel(options)voidAdd a top-bar icon button that toggles a fixed panel anchored under the button.
addFloatingPanel(options)voidAdd a top-bar icon button that toggles a draggable, resizable fixed panel.
registerComponent(placement, component)voidRegister a React component under a placement.
unregisterComponent(placement, componentId)voidRemove one component from a placement by id.
getComponents(placement)UIComponent[]The components registered under a placement.
forceUpdate(placement?)voidRe-render every subscribed placement host.
setTheme(theme)voidSet 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)voidSet the --accent CSS variable on the root element.
setPrimaryColor(hsl)voidSet the --primary CSS variable on the root element.
setCSSVariable(name, value)voidSet a CSS variable on the root element.
resetColors()voidRemove 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)booleanWhether a map layer is visible.
setLayerVisibility(layerName, visible)voidSet one map layer's visibility in the store.
getAllLayerVisibility()Record<string, boolean>The visibility of every map layer.
setMultipleLayerVisibility(layers)voidSet several map layers' visibility in one store write.

Notifications#

showNotification#

api.ui.showNotification(message: string, type?: 'info' | 'success' | 'error' | 'warning', title?: string): void

Show 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.

ParamTypeDefaultDescription
messagestringrequiredtoast body text.
type'info' or 'success' or 'error' or 'warning'noneinfo, success, error or warning; default info.
titlestringnonetoast 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredbutton definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within the placement; a repeat id replaces the earlier entry.
labelstringrequiredButton text.
iconstringnoneLucide icon name rendered at 14 px before the label; ignored in the three menu placements; an unknown name renders no icon.
onClick() => voidrequiredClick 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredtoggle definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within the placement; a repeat id replaces the earlier entry.
labelstringrequiredText after the checkbox.
defaultValuebooleanfalseInitial checked state.
onChange(value: boolean) => voidrequiredCalled 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredslider definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within the placement; a repeat id replaces the earlier entry.
labelstringrequiredLabel text; rendered as <label>: <value>.
minnumberrequiredLower bound of the range input; also the initial value when defaultValue is omitted.
maxnumberrequiredUpper bound of the range input.
stepnumber1Range input step.
defaultValuenumberminInitial value.
onChange(value: number) => voidrequiredCalled 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredselect definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within the placement; a repeat id replaces the earlier entry.
labelstringrequiredLabel text above the select.
options{ value: string; label: string }[]requiredOption list; each entry renders <option value={value}>{label}</option>.
defaultValuestringnoneInitial selected value; when omitted the first option's value is used, or the empty string for an empty list.
onChange(value: string) => voidrequiredCalled 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredtext definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within the placement; a repeat id replaces the earlier entry.
textstringrequiredParagraph content.
classNamestringtext-sm text-muted-foregroundClass 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement id.
optionsobjectrequiredseparator definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
optionsobjectrequiredbutton definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within top-bar; a repeat id replaces the earlier entry.
iconstringrequiredLucide icon name; rendered at 20 px (w-5 h-5); an unknown name renders nothing.
tooltipstring""Native title attribute text.
onClick() => voidrequiredClick handler.
isActive() => booleannonePolled 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 found at render (on every 500 ms poll when isActive is 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).

js
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; }): void

Add 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.

ParamTypeDefaultDescription
optionsobjectrequiredpanel definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within top-bar; a repeat id replaces the earlier entry; shown as the header when title is omitted.
iconstringrequiredLucide icon name rendered at 20 px; an unknown name renders nothing.
tooltipstringnoneNative title attribute on the button.
titlestringidHeader text.
widthnumber380Panel width; px; 0 falls back to the default.
renderReact.ComponentTyperequiredComponent rendered in the panel body with no props.

Errors:

  • icon name not in Lucide: logs [Modding API] Icon "<icon>" not found at 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).

js
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: {…}): void

Add 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.

ParamTypeDefaultDescription
optionsobjectrequiredpanel definition.

options fields:

FieldTypeDefaultDescription
idstringrequiredRegistry id within top-bar and the localStorage key suffix floating-panel-<id>; shown as the header when title is omitted.
iconstringrequiredLucide icon name rendered at 20 px; an unknown name renders nothing.
tooltipstringnoneNative title attribute on the button; when omitted, title is used, then the empty string.
titlestringidHeader text.
defaultWidthnumber500Initial width when no saved state exists; px; 0 falls back to the default.
defaultHeightnumber400Initial height when no saved state exists; px; 0 falls back to the default.
minWidthnumber300Lower bound while resizing; px; 0 falls back to the default.
minHeightnumber200Lower bound while resizing; px; 0 falls back to the default.
renderReact.ComponentType<{ width: number; height: number }>requiredComponent 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 found at 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).

js
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): void

Register 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredtarget placement id.
componentUIComponentrequiredentry 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).

js
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): void

Remove 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.

ParamTypeDefaultDescription
placementUIPlacementrequiredplacement the component was registered under.
componentIdstringrequiredthe 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.

js
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 [].

js
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): void

Re-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).

js
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'): void

Set 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).

js
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.

js
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.

js
// the theme actually shown, with 'system' resolved
const colors = api.gameState.getMapColors(api.ui.getResolvedTheme() === 'dark');

Color Customization#

setAccentColor#

api.ui.setAccentColor(hsl: string): void

Set 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).

js
api.ui.setAccentColor('210 100% 50%'); // HSL triple, no hsl() wrapper

setPrimaryColor#

api.ui.setPrimaryColor(hsl: string): void

Set 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).

js
api.ui.setPrimaryColor('142 71% 45%');

setCSSVariable#

api.ui.setCSSVariable(name: string, value: string): void

Set 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.

ParamTypeDefaultDescription
namestringrequiredvariable name with or without --.
valuestringrequiredCSS 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).

js
api.ui.setCSSVariable('--my-mod-highlight', '48 96% 53%');
api.ui.setCSSVariable('radius', '0.25rem'); // the -- prefix is added

resetColors#

api.ui.resetColors(): void

Remove 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).

js
api.ui.setAccentColor('0 84% 60%');
api.ui.resetColors(); // back to the theme defaults

Layer 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 [].

js
const layers = api.ui.getAvailableLayers();
console.log(layers.includes('trackSpeeds'));

getLayerVisibility#

api.ui.getLayerVisibility(layerName: string): boolean

Whether 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.

js
if (api.ui.getLayerVisibility('trains')) console.log('Trains are shown');

setLayerVisibility#

api.ui.setLayerVisibility(layerName: string, visible: boolean): void

Set one map layer's visibility in the store.

The store receives a new layers object with the single key changed.

ParamTypeDefaultDescription
layerNamestringrequireda name from getAvailableLayers.
visiblebooleanrequirednew 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).

js
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 {}.

js
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>): void

Set 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).

js
// 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.

FieldTypeDefaultDescription
idstringrequiredUnique id within the placement; a repeat id replaces the earlier entry.
componentReact.ComponentType<any>requiredFunction component rendered with props. See UIComponent.component below.
propsRecord<string, any>{}Props passed to a function component.
modIdstringnoneOwning 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.