Theming
The entire ApexOS UI is styled through CSS custom properties. A theme is nothing more than a set of values for these properties; the frontend applies them by setting the variables on the document (or on an individual element, when a theme is scoped to a dashboard, view, or card).
Core theme variables
The default values live in the frontend source under src/resources/styles/ and src/resources/theme/ (split into core, main, typography, semantic, and color layers, plus animation styles). The variables you will most often override:
Brand and text colors
--primary-color: var(--ha-color-primary-40); /* resolved from the design-token palette */
--accent-color: #ff9800;
--primary-text-color: var(--ha-color-text-primary);
--secondary-text-color: var(--ha-color-text-secondary);
--disabled-text-color: #bdbdbd;
--divider-color: rgba(0, 0, 0, 0.12);
--state-icon-color: #44739e;
The high-level variables are backed by a lower design-token layer (--ha-color-* palette tokens such as --ha-color-primary-40). Override the high-level names shown here; the token layer is an implementation detail.
Backgrounds
--card-background-color: #ffffff;
--primary-background-color: #fafafa;
--secondary-background-color: #e5e5e5;
Derived surfaces
Many component variables are defined in terms of the core set, so overriding the core variables re-skins most of the UI automatically. Examples from the source:
--sidebar-text-color: var(--primary-text-color);
--sidebar-background-color: var(--card-background-color);
--sidebar-selected-text-color: var(--primary-color);
--sidebar-selected-icon-color: var(--primary-color);
--slider-color: var(--primary-color);
--table-row-background-color: var(--primary-background-color);
--table-row-alternative-background-color: var(--secondary-background-color);
--data-table-background-color: var(--card-background-color);
--markdown-code-background-color: var(--primary-background-color);
--app-header-border-bottom: 1px solid var(--divider-color);
The Material component layer is bridged the same way (--mdc-theme-primary: var(--primary-color), --mdc-theme-surface: var(--card-background-color), and so on), so themes do not need to target it directly.
Dark mode
The frontend ships a built-in dark variant. When dark mode is active, a dark override set is applied on top of the base variables:
--primary-background-color: #111111;
--card-background-color: #1c1c1c;
--secondary-background-color: #282828;
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--divider-color: rgba(225, 225, 225, 0.12);
Dark mode follows the operating system by default: the frontend watches the (prefers-color-scheme: dark) media query and re-applies themes when it changes. Users can pin light or dark (or leave it on auto) from the theme picker in their profile.
How themes are defined and applied
Themes are configured on the instance and delivered to the frontend over the WebSocket API (frontend/get_themes, with live updates via the themes_updated event). The payload looks like:
interface Themes {
default_theme: string;
default_dark_theme: string | null;
themes: Record<string, Theme>;
darkMode: boolean; // currently effective dark mode
theme: string; // currently active theme name
}
Each theme is a flat map of variable names (without the -- prefix) to values, optionally with per-mode overrides:
type Theme = ThemeVars & {
modes?: {
light?: ThemeVars;
dark?: ThemeVars;
};
};
A theme that defines modes.dark is considered dark-mode capable; the matching mode's variables are merged over the theme's base variables when applied.
When a theme is applied, the frontend converts each entry to a ---prefixed CSS custom property and sets it as an inline style on the target element. For the default theme it additionally derives an accessible color palette from the selected primary and accent colors (shades, contrast text colors, and related derived variables are computed automatically).
Themes can be applied at several scopes:
- Globally — the active theme from the settings (per-user selection in the profile, falling back to
default_theme/default_dark_theme). - Per element — dashboards, views, and cards that support a
themeoption apply the named theme to just their own subtree via the same mechanism.