Skip to main content

Theming & UI

OpenRobOps ships with a multi-theme UI built on Material UI (MUI) v7 and a design-token system. Users pick a theme visually, the choice is stored per user, and switching applies live — no page reload.

Built-in themes

ThemeModeInspired by
ORO Moon (default)darkThe ORO brand palette: slate, gold, teal
ORO SunlightORO Moon's light scheme for bright floors and printed reports
Tokyo Nightdarkfolke/tokyonight.nvim
Tokyo DaylightTokyo Night's day style
Catppuccin Mochadarkcatppuccin/palette
Catppuccin LattelightCatppuccin's light flavor
Rosé Pine Moondarkrose-pine/palette
Rosé Pine DawnlightRosé Pine's light variant
MonokaidarkThe classic editor palette

Selecting a theme

Themes are selected in Settings → Appearance, which shows an Auto (match browser) card followed by live preview cards grouped into Dark themes and Light themes rows (each rendered with that theme's real component styles). Clicking a card applies the theme immediately and saves it to the user's profile; the grid is keyboard-accessible (arrow keys move and select, like a radio group).

The theme is resolved with the following precedence:

  1. URL parameterhttps://your-app/?theme=monokai. Handy for testing and sharing links; overrides everything else and is not persisted.

  2. User preference — whatever the user picked in Settings → Appearance. Stored per user in the preferences collection (ui.theme, written by the preferences.setUserUi method) and applied automatically after login — including in other open tabs, which pick the change up live through the userPreferences subscription.

  3. Auto — the default when the user hasn't picked a theme (or picked "Auto"): the browser's prefers-color-scheme chooses between the dark and light defaults, live (changing the OS scheme re-themes the app). The defaults are oro (dark) and oro-sun (light), optionally overridden per deployment in settings.json:

    {
    "public": {
    "defaultTheme": "catppuccin-mocha",
    "defaultLightTheme": "catppuccin-latte"
    }
    }

Valid theme names are the keys of the THEMES registry in app/imports/client/Styles.js: oro, oro-sun, monokai, tokyo-night, tokyo-day, catppuccin-mocha, catppuccin-latte, rose-pine-moon, rose-pine-dawn.

How theming works

Colors live in design-token files under app/imports/client/themes/, one per theme. oro.js is the complete token contract — every color the app uses comes from it (grouped as text, background, incidents, teleop, map, severityColor, and so on). Other themes override only what differs and inherit the rest:

// app/imports/client/themes/monokai.js (excerpt)
import { cloneDeep, merge } from 'lodash';
import oro from './oro';

const monokai = merge(cloneDeep(oro), {
mode: 'dark',
text: { primary: '#F8F8F2' /* ... */ },
background: { default: '#272822' /* ... */ },
secondary: { main: '#A6E22E' },
// anything not overridden inherits from oro
});
export default monokai;

app/imports/client/Styles.js builds a full MUI theme from the active token set (palette, component style overrides, and the CSS custom properties that plain CSS files use, e.g. --color-background). It keeps the current theme in module state and exposes:

  • setTheme(name) — hot-applies a theme and notifies subscribers
  • useOroTheme() — React hook the app root uses to re-render on switch
  • getThemeInstance(name) — cached built theme per name (used by the Settings preview cards through nested ThemeProviders)
  • default export — a live proxy view of the current theme, for the few non-React modules (OpenLayers map layers, some SVG icon modules) that import the theme directly

Hot switching restyles everything MUI/emotion renders. Widgets that paint imperatively (OpenLayers maps) pick the new colors up when they next mount, i.e. on the next route navigation.

Adding a theme

  1. Create a token file in app/imports/client/themes/ following the pattern above: define named consts for the source palette, merge over oro, and override at least the text, background, status (modes, incidents, diagnostics, snackbar), accent (secondary.main and friends) and severityColor groups. Every theme must also explicitly define the accent contract pair: text.onAccent (ink for text/icons sitting on an accent fill — never for strokes or the logo) and background.accentSolid (the solid accent fill it sits on).
  2. Register it in the THEMES object in app/imports/client/Styles.js. The key becomes the URL/preference name; the Settings selector labels it by capitalizing hyphenated words (tokyo-night → "Tokyo Night") and shows a live preview automatically.

For light themes, override everything that assumes a dark background — all text slots, surfaces/borders, shadowColor.white (use a dark tint), and any pastel tags/zeroData colors that would wash out. Set mode: 'light' so MUI components follow along. The app logo needs no per-mode asset: OroLogo draws its ink from text.primary and its wedge from secondary.main, so it adapts to any theme automatically.

Rules for themable UI code

  • Never hardcode colors in components. Reference theme tokens via sx, makeStyles from tss-react/mui, or useTheme(). If no token fits, add one to oro.js (all themes inherit it) rather than inlining a hex.
  • Never capture theme values in module-level constants — that freezes the load-time theme and breaks hot switching. Read theme.palette.* at render or call time instead.
  • Include theme (or classes) in memo dependencies when a useMemo/useCallback bakes theme colors into data or renderers (Plotly layouts, react-calendar-timeline renderers). Otherwise the widget keeps the old theme's styles until remount.
  • Compute on-color text with theme.palette.getContrastText(bg) instead of assuming black or white (see the severity chips) — it stays readable for any theme's palette.
  • Text on themed surfaces is text.primary (or an alpha(theme.palette.text.primary, x) dim of it), never common.white / rgba(255,255,255,…) — those break on light themes.
  • Plain CSS files should use the theme-synced custom properties (--color-background, --color-card, --color-border, and --map-image-filter for theme-aware map inversion) that Styles.js injects on :root, as app/imports/client/lib/IncidentTimeline.css does.

Styling reference

Component-level styling uses tss-react/mui:

import { makeStyles } from 'tss-react/mui';

const useStyles = makeStyles()((theme) => ({
myComponent: {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
padding: theme.spacing(2),
},
}));
LibraryPurpose
React 18UI framework
Material UI 7Component library and theming
tss-react / @emotionCSS-in-JS styling
lucide-react / @mui/icons-materialIcons

Next Steps