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
| Theme | Mode | Inspired by |
|---|---|---|
| ORO Moon (default) | dark | The ORO brand palette: slate, gold, teal |
| ORO Sun | light | ORO Moon's light scheme for bright floors and printed reports |
| Tokyo Night | dark | folke/tokyonight.nvim |
| Tokyo Day | light | Tokyo Night's day style |
| Catppuccin Mocha | dark | catppuccin/palette |
| Catppuccin Latte | light | Catppuccin's light flavor |
| Rosé Pine Moon | dark | rose-pine/palette |
| Rosé Pine Dawn | light | Rosé Pine's light variant |
| Monokai | dark | The 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:
-
URL parameter —
https://your-app/?theme=monokai. Handy for testing and sharing links; overrides everything else and is not persisted. -
User preference — whatever the user picked in Settings → Appearance. Stored per user in the
preferencescollection (ui.theme, written by thepreferences.setUserUimethod) and applied automatically after login — including in other open tabs, which pick the change up live through theuserPreferencessubscription. -
Auto — the default when the user hasn't picked a theme (or picked "Auto"): the browser's
prefers-color-schemechooses between the dark and light defaults, live (changing the OS scheme re-themes the app). The defaults areoro(dark) andoro-sun(light), optionally overridden per deployment insettings.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 subscribersuseOroTheme()— React hook the app root uses to re-render on switchgetThemeInstance(name)— cached built theme per name (used by the Settings preview cards through nestedThemeProviders)- 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
- Create a token file in
app/imports/client/themes/following the pattern above: define named consts for the source palette,mergeoveroro, and override at least thetext,background, status (modes,incidents,diagnostics,snackbar), accent (secondary.mainand friends) andseverityColorgroups. 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) andbackground.accentSolid(the solid accent fill it sits on). - Register it in the
THEMESobject inapp/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,makeStylesfromtss-react/mui, oruseTheme(). If no token fits, add one tooro.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(orclasses) in memo dependencies when auseMemo/useCallbackbakes 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 analpha(theme.palette.text.primary, x)dim of it), nevercommon.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-filterfor theme-aware map inversion) thatStyles.jsinjects on:root, asapp/imports/client/lib/IncidentTimeline.cssdoes.
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),
},
}));
| Library | Purpose |
|---|---|
| React 18 | UI framework |
| Material UI 7 | Component library and theming |
| tss-react / @emotion | CSS-in-JS styling |
| lucide-react / @mui/icons-material | Icons |
Next Steps
- Custom Widgets — build new widget components
- Project Structure — where UI code lives
- Dashboards & Widgets — available widgets