UI Components Guide
Build custom user interfaces for your mod
This guide shows you how to create custom UI elements for your mod, from simple buttons to complex React components.
Quick UI Primitives#
For simple UI needs, use the built-in primitives without any React knowledge:
Settings Panel#
Add controls to your mod's settings section:
javascript
const api = window.SubwayBuilderAPI;
// Add a toggle for your mod's main feature
api.ui.addToggle('settings-menu', {
id: 'my-mod-enabled',
label: 'Enable My Mod',
defaultValue: true,
onChange: (enabled) => {
console.log('Mod enabled:', enabled);
}
});
// Add a slider for a configurable value
api.ui.addSlider('settings-menu', {
id: 'bonus-amount',
label: 'Daily Bonus Amount',
min: 10000,
max: 1000000,
step: 10000,
defaultValue: 100000,
onChange: (value) => {
myMod.bonusAmount = value;
}
});
// Add a dropdown for mode selection
api.ui.addSelect('settings-menu', {
id: 'difficulty',
label: 'Difficulty',
options: [
{ value: 'easy', label: 'Easy' },
{ value: 'normal', label: 'Normal' },
{ value: 'hard', label: 'Hard' }
],
defaultValue: 'normal',
onChange: (value) => {
myMod.difficulty = value;
}
});
// Add a separator and info text
api.ui.addSeparator('settings-menu', { id: 'sep1' });
api.ui.addText('settings-menu', {
id: 'version-info',
text: 'My Mod v1.0.0 by YourName',
className: 'text-xs text-muted-foreground'
});Toolbar Buttons#
Add buttons to the top toolbar:
Simple Button#
javascript
api.ui.addToolbarButton({
id: 'quick-save',
icon: 'Save',
tooltip: 'Quick Save',
onClick: () => {
api.ui.showNotification('Game saved!', 'success');
}
});Button with Active State#
javascript
let isActive = false;
api.ui.addToolbarButton({
id: 'auto-mode',
icon: 'Zap',
tooltip: 'Toggle Auto Mode',
onClick: () => {
isActive = !isActive;
api.ui.showNotification(
isActive ? 'Auto mode ON' : 'Auto mode OFF',
'info'
);
},
isActive: () => isActive
});Button with Panel#
javascript
api.ui.addToolbarPanel({
id: 'my-dashboard',
icon: 'BarChart3',
tooltip: 'My Dashboard',
title: 'Statistics Dashboard',
width: 400,
render: () => {
const h = api.utils.React.createElement;
const stations = api.gameState.getStations();
const routes = api.gameState.getRoutes();
return h('div', { className: 'space-y-4 p-4' }, [
h('div', { key: 'stations' }, `Stations: ${stations.length}`),
h('div', { key: 'routes' }, `Routes: ${routes.length}`),
h('div', { key: 'budget' }, `Budget: $${api.gameState.getBudget().toLocaleString()}`)
]);
}
});Styled Components#
Use the game's styled components for a polished look:
javascript
api.ui.addStyledButton('settings-menu', {
id: 'reset-button',
label: 'Reset Settings',
icon: 'RefreshCw',
variant: 'destructive',
onClick: () => {
// Reset logic
}
});
api.ui.addStyledSlider('settings-menu', {
id: 'speed-multiplier',
label: 'Speed Multiplier',
min: 0.5,
max: 3.0,
step: 0.1,
defaultValue: 1.0,
showValue: true,
unit: 'x',
onChange: (value) => {
myMod.speedMultiplier = value;
}
});Custom React Components#
For complex UIs, create full React components:
javascript
const { React, components, icons } = api.utils;
const h = React.createElement;
const { Card, CardHeader, CardTitle, CardContent, Button, Badge } = components;
const { Train, MapPin, DollarSign } = icons;
function MyStatsPanel() {
const stations = api.gameState.getStations();
const routes = api.gameState.getRoutes();
const budget = api.gameState.getBudget();
return h(Card, { className: 'w-full' }, [
h(CardHeader, { key: 'header' },
h(CardTitle, { className: 'flex items-center gap-2' }, [
h(Train, { key: 'icon', className: 'h-5 w-5' }),
'Network Stats'
])
),
h(CardContent, { key: 'content', className: 'space-y-3' }, [
// Stations row
h('div', {
key: 'stations',
className: 'flex items-center justify-between'
}, [
h('span', { className: 'flex items-center gap-2' }, [
h(MapPin, { className: 'h-4 w-4 text-muted-foreground' }),
'Stations'
]),
h(Badge, { variant: 'secondary' }, stations.length)
]),
// Budget row
h('div', {
key: 'budget',
className: 'flex items-center justify-between'
}, [
h('span', { className: 'flex items-center gap-2' }, [
h(DollarSign, { className: 'h-4 w-4 text-muted-foreground' }),
'Budget'
]),
h(Badge, {
variant: budget > 0 ? 'default' : 'destructive'
}, `$${budget.toLocaleString()}`)
]),
// Action button
h(Button, {
key: 'action',
className: 'w-full mt-4',
onClick: () => api.ui.showNotification('Stats refreshed!', 'success')
}, 'Refresh Stats')
])
]);
}
// Register the component
api.ui.registerComponent('settings-menu', {
id: 'my-stats-panel',
component: MyStatsPanel
});Main Menu Button#
Add a button to the main menu:
javascript
api.ui.addMainMenuButton({
id: 'my-mod-menu',
text: 'My Mod',
description: 'Open mod settings',
arrowBearing: 45,
onClick: () => {
// Open your mod's main interface
console.log('Menu button clicked');
}
});Theme Control#
Programmatically control the theme:
javascript
// Day/night cycle based on in-game time
api.hooks.onDayChange((day) => {
const hour = api.gameState.getCurrentHour();
if (hour >= 6 && hour < 18) {
api.ui.setTheme('light');
} else {
api.ui.setTheme('dark');
}
});
// Color customization
api.ui.setAccentColor('#8b5cf6'); // Purple
api.ui.setPrimaryColor('#0ea5e9'); // Sky blueAvailable Icons#
api.utils.icons includes a curated Lucide set that is guaranteed to exist at runtime. This list is the source of truth (mirrors next-app-2/app/game/moddingAPI/curatedIcons.ts):
javascript
const availableIcons = [
'Activity',
'AlertCircle',
'AlertOctagon',
'AlertTriangle',
'AlignCenter',
'AlignJustify',
'AlignLeft',
'AlignRight',
'ArrowDown',
'ArrowDownLeft',
'ArrowDownRight',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowUpLeft',
'ArrowUpRight',
'BarChart',
'BarChart2',
'Battery',
'BatteryCharging',
'Bell',
'BellOff',
'Bike',
'Building',
'Building2',
'Bus',
'Calendar',
'CalendarCheck',
'CalendarDays',
'CalendarX',
'Camera',
'Car',
'Check',
'CheckCircle',
'ChevronDown',
'ChevronLeft',
'ChevronRight',
'ChevronUp',
'Clipboard',
'Clock',
'Cloud',
'CloudLightning',
'CloudRain',
'CloudSnow',
'Compass',
'Copy',
'CreditCard',
'DollarSign',
'Download',
'Edit',
'ExternalLink',
'Eye',
'EyeOff',
'FastForward',
'Filter',
'FilterX',
'Gauge',
'Globe',
'Grid',
'Headphones',
'Heart',
'HelpCircle',
'Home',
'Image',
'ImageOff',
'Info',
'Key',
'Layers',
'LineChart',
'Link',
'List',
'Locate',
'LocateFixed',
'LocateOff',
'Lock',
'Mail',
'MailOpen',
'Map',
'MapPin',
'MapPinOff',
'Maximize',
'Menu',
'MessageCircle',
'MessageSquare',
'Mic',
'MicOff',
'Minimize',
'Minus',
'Monitor',
'Moon',
'Move',
'Navigation',
'Pause',
'PauseCircle',
'Percent',
'Phone',
'PhoneCall',
'PieChart',
'Pin',
'Plane',
'Play',
'PlayCircle',
'Plus',
'Power',
'Receipt',
'RefreshCw',
'Rewind',
'RotateCcw',
'Route',
'Save',
'Search',
'Settings',
'Share',
'Shield',
'ShieldCheck',
'Ship',
'ShoppingCart',
'SkipBack',
'SkipForward',
'SlidersHorizontal',
'SlidersVertical',
'Smartphone',
'SortAsc',
'SortDesc',
'Star',
'StopCircle',
'Sun',
'Tablet',
'Ticket',
'Timer',
'Train',
'TrendingDown',
'TrendingUp',
'Trash2',
'Triangle',
'Unlock',
'Upload',
'User',
'UserCheck',
'UserMinus',
'UserPlus',
'UserX',
'Users',
'Video',
'VideoOff',
'Volume',
'Volume1',
'Volume2',
'VolumeX',
'Wind',
'X',
'XCircle',
'Zap',
];javascript
const { icons } = api.utils;
const { Train, MapPin, DollarSign, Settings, Play, Pause } = icons;Best Practices#
- Use unique IDs - Prefix IDs with your mod name
- Handle errors - Wrap UI logic in try-catch
- Clean up - Remove components on mod unload
- Be responsive - Test at different window sizes
- Match the style - Use the game's components when possible