How to theme Domoticz: Difference between revisions
No edit summary |
No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 1: | Line 1: | ||
<blockquote>'''Note:''' This page is maintained in the [https://github.com/domoticz/domoticz/tree/development/docs Domoticz GitHub repository]. Please do not edit it directly on the Wiki.</blockquote> | <blockquote>'''Note:''' This page is maintained in the [https://github.com/domoticz/domoticz/tree/development/docs Domoticz GitHub repository]. Please do not edit it directly on the Wiki.</blockquote> | ||
'''Revision:''' 2026- | '''Revision:''' 2026-04-23<br> | ||
== Introduction == | == Introduction == | ||
| Line 322: | Line 321: | ||
The <code>--dz-btn-*</code> variables control Bootstrap <code>.btn*</code> styles. The <code>.btnstyle*</code> and <code>.btnsmall*</code> systems do not yet have variables — override them with explicit selectors if needed. | The <code>--dz-btn-*</code> variables control Bootstrap <code>.btn*</code> styles. The <code>.btnstyle*</code> and <code>.btnsmall*</code> systems do not yet have variables — override them with explicit selectors if needed. | ||
== Theme settings storage == | |||
Themes can persist their own configuration in the Domoticz database without using user variables. The settings are stored in the <code>Preferences</code> table under the key <code>ThemeSettings</code> as a single JSON object, and are included in the standard <code>getsettings</code> / <code>storesettings</code> API used by the Settings page. | |||
=== How it works === | |||
When the Settings page loads, <code>getsettings</code> returns a <code>ThemeSettings</code> object in the JSON response. This object is exposed on the AngularJS scope as <code>$scope.ThemeSettings</code>. The currently active theme name is available on the same scope as <code>$scope.WebTheme</code>. | |||
By convention, the top-level keys of <code>ThemeSettings</code> are theme names. This means multiple themes can store their settings in the same object without overwriting each other — switching between themes leaves every theme's settings intact. | |||
When the user saves the Settings page, the current value of <code>$scope.ThemeSettings</code> is serialised and submitted as part of the normal <code>storesettings</code> POST. No extra API calls are required. | |||
=== Reading settings in custom.js === | |||
<code>custom.js</code> is loaded on every page. To read settings, get the AngularJS scope from any page that has one (the main content area), then look up your theme by name: | |||
<source lang="javascript"> | |||
var THEME_NAME = 'YourTheme'; // must match the folder name under www/styles/ | |||
function getThemeSettings() { | |||
var el = document.getElementById('maindiv') || document.body; | |||
var scope = angular.element(el).scope(); | |||
if (!scope) return {}; | |||
return (scope.ThemeSettings && scope.ThemeSettings[THEME_NAME]) || {}; | |||
} | |||
</source> | |||
On pages that use a different root element the scope lookup may return nothing — always guard with a null check. | |||
=== Writing settings back === | |||
Update <code>$scope.ThemeSettings[THEME_NAME]</code> and the value will be included the next time the user saves Settings. If your theme provides its own Save button, you can also trigger the settings save directly: | |||
<source lang="javascript"> | |||
var THEME_NAME = 'YourTheme'; | |||
function saveThemeSettings(newSettings) { | |||
var el = document.getElementById('maindiv') || document.body; | |||
var scope = angular.element(el).scope(); | |||
if (!scope) return; | |||
scope.$apply(function () { | |||
scope.ThemeSettings = scope.ThemeSettings || {}; | |||
scope.ThemeSettings[THEME_NAME] = newSettings; | |||
}); | |||
} | |||
</source> | |||
To persist without requiring the user to click the Settings save button, call <code>scope.StoreSettings()</code> after updating — but only do this from within the Settings page (<code>#Setup</code> route), not from general page scripts. | |||
=== Getting the active theme name === | |||
<code>$scope.WebTheme</code> holds the name of the currently active theme (the folder name under <code>www/styles/</code>, e.g. <code>Nightglass</code>). Your theme's <code>custom.js</code> already knows its own name, so hardcoding it is fine. <code>$scope.WebTheme</code> is useful if you write shared utility code that needs to be theme-agnostic. | |||
=== Example: dark/light mode toggle === | |||
<source lang="javascript"> | |||
var THEME_NAME = 'YourTheme'; | |||
var DEFAULTS = { style: 'dark', aceTheme: 'ace/theme/tomorrow_night' }; | |||
function getSettings() { | |||
var el = document.getElementById('maindiv') || document.body; | |||
var scope = angular.element(el).scope(); | |||
if (!scope || !scope.ThemeSettings) return Object.assign({}, DEFAULTS); | |||
return Object.assign({}, DEFAULTS, scope.ThemeSettings[THEME_NAME]); | |||
} | |||
function setStyle(style) { | |||
var el = document.getElementById('maindiv') || document.body; | |||
var scope = angular.element(el).scope(); | |||
if (!scope) return; | |||
scope.$apply(function () { | |||
scope.ThemeSettings = scope.ThemeSettings || {}; | |||
scope.ThemeSettings[THEME_NAME] = Object.assign(getSettings(), { style: style }); | |||
}); | |||
applyStyle(style); | |||
} | |||
</source> | |||
=== Storage limits === | |||
The <code>ThemeSettings</code> value is stored as a text field in SQLite. There is no hard size limit enforced at the application level, but keep the JSON compact — a few kilobytes is plenty for theme configuration. Do not store binary data or per-device state here. | |||
=== Accessing settings outside the Settings page === | |||
On pages other than Settings, <code>$scope.ThemeSettings</code> is not automatically populated. For applying settings on every page load (e.g. re-applying a dark/light mode), read the settings via a direct API call on page load: | |||
<source lang="javascript"> | |||
(function () { | |||
$.getJSON('json.htm?type=command¶m=getsettings', function (data) { | |||
var settings = (data.ThemeSettings && data.ThemeSettings[THEME_NAME]) || DEFAULTS; | |||
applyStyle(settings.style); | |||
}); | |||
})(); | |||
</source> | |||
This call is unauthenticated-safe for read-only access on the local network. | |||
== Known limitations == | == Known limitations == | ||
Latest revision as of 10:49, 23 April 2026
Note: This page is maintained in the Domoticz GitHub repository. Please do not edit it directly on the Wiki.
Revision: 2026-04-23
Introduction
Domoticz supports visual themes that let you change the look of the web interface without modifying core files.
The active theme is configured in Setup → Settings → System → GUI theme and defaults to default.
How URL rewriting works
index.html always loads styles/default/custom.css. The Domoticz web server (cWebem) intercepts every request for a file under styles/default/ and transparently serves the corresponding file from the active theme folder if it exists there. If the file does not exist in the active theme folder, the request falls through to styles/default/.
This means:
- A theme only needs to provide files it actually overrides.
common.css,variables.css, andlegacy.cssare served fromdefault/unless your theme provides its own copies.- Fallback is per-file — you can override
dark.cssalone without touching anything else.
Minimum required files
Every theme needs exactly one file: custom.css.
Dark theme
@import url("../../css/legacy.css"); /* required */
@import url("common.css"); /* shared utilities — served from default/ via fallback */
@import url("dark.css"); /* your dark :root overrides */
Light theme
@import url("../../css/legacy.css"); /* required */
@import url("common.css"); /* shared utilities — light by default, no dark.css needed */
For a light theme you do not need a dark.css. The --dz-* variable defaults defined in variables.css are light-valued, so form controls, modals, and buttons render correctly without any :root override block.
CSS load order
The following shows the full waterfall from index.html to theme variables. Theme authors need to understand what loads when and which :root block wins.
index.html → <link> files loaded in parallel:
css/style.css (structural resets + body { background: var(--dz-body-bg) })
css/bootstrap.css
css/bootstrap-responsive.css
... other lib CSS ...
styles/default/custom.css (last <link>; server-rewritten to active theme)
└─ @import legacy.css (serial fetch 1)
│ └─ @import variables.css (serial fetch 2 — --dz-* light defaults + widget rules)
├─ @import common.css (serial fetch 3 — colour-neutral utilities)
├─ @import dark.css (serial fetch 4 — dark :root overrides, dark themes only)
└─ theme's own :root block (inline in custom.css — wins by source order)
What wins (specificity rules)
- All
:rootblocks have the same specificity(0,1,0)— last one wins by source order. variables.css:rootloads first →dark.css:rootoverrides it → theme's inline:rootoverrides that.- For non-variable rules, normal CSS specificity applies.
- Inline styles from JavaScript (
$(el).css(...)) override everything, including variables.
CSS variable reference
All --dz-* variables are defined in css/variables.css with light values as their defaults. These variables form the stable public theming API.
The default Domoticz theme uses light widget colours on a dark page background. default/dark.css provides the dark body gradient, dark form controls, modals, buttons, and content panels. Widget card colours are intentionally left at the light defaults from variables.css.
The "Suggested dark value" column in the tables below shows recommended values for theme authors building a fully dark theme. They are not defined anywhere by default — you must set them yourself in your theme's :root block or dark.css.
Widget card
| Variable | Light default | Suggested dark value | Description |
|---|---|---|---|
--dz-widget-bg |
#F1F5FA |
#11213d |
Device card background |
--dz-widget-text |
#0D161F |
#fff |
Device card text |
--dz-widget-border-radius |
5px |
5px |
Card corner radius |
--dz-widget-shadow |
none |
none |
Card box-shadow |
--dz-widget-name-bg |
#D4E1EE |
#283750 |
Name bar background |
--dz-widget-name-border |
#2B5074 |
#404C63 |
Name bar border |
--dz-widget-name-text |
#0D161F |
#fff |
Name bar text |
--dz-widget-name-font-size |
130% |
130% |
Name bar font size |
--dz-widget-status-text |
#000000 |
#eee |
Value/status text |
--dz-widget-value-font-size |
140% |
140% |
Value column font size |
--dz-widget-hover-bg |
#F2F1FA |
#1a2e4a |
Card background on hover |
--dz-widget-hover-name-bg |
#D4D5EE |
#3a4d6a |
Name bar background on hover |
--dz-widget-hover-name-border |
#2B2C74 |
#555c7a |
Name bar border on hover |
--dz-widget-hover-name-text |
#0D0D1F |
#fff |
Name bar text on hover |
Status colours
| Variable | Light default | Suggested dark value | Description |
|---|---|---|---|
--dz-status-normal |
#D4E1EE |
#283750 |
Normal / no alert |
--dz-status-protected |
#A4B1EE |
#354a80 |
Protected device |
--dz-status-timeout |
#DF2D3A |
#8b1a23 |
Communication timeout |
--dz-status-low-battery |
#DDDF2D |
#7a7a00 |
Low battery |
--dz-status-disabled |
#A6A8AA |
#4a4a4a |
Disabled |
--dz-status-disabled-opacity |
0.5 |
0.5 |
Disabled opacity |
Page, forms, modals, navigation, and buttons
The light default for --dz-body-bg is intentionally set to #202020 (a dark value) to prevent a flash of unstyled content (FOUC) on page load before the theme's CSS arrives. Light themes should always override this variable.
| Variable | Light default | Description |
|---|---|---|
--dz-body-bg |
#202020 |
Page background (dark default to prevent FOUC) |
--dz-body-text |
#fff |
Page text |
--dz-input-bg |
#fff |
Input / select / textarea background |
--dz-input-text |
#000 |
Input text |
--dz-input-border |
#ccc |
Input border |
--dz-modal-bg |
#fff |
Modal background |
--dz-modal-text |
#000 |
Modal text |
--dz-modal-header-bg |
#f5f5f5 |
Modal header background |
--dz-nav-bg |
#283750 |
Navbar background |
--dz-btn-bg |
#f5f5f5 |
Button background |
--dz-btn-text |
#333 |
Button text |
--dz-btn-border |
#ccc |
Button border |
--dz-btn-hover-bg |
#e0e0e0 |
Button hover background |
--dz-btn-primary-bg |
#337ab7 |
Primary button background |
--dz-btn-warning-bg |
#f0ad4e |
Warning button background |
--dz-btn-danger-bg |
#d9534f |
Danger button background |
--dz-btn-info-bg |
#5bc0de |
Info button background |
--dz-btn-success-bg |
#5cb85c |
Success button background |
--dz-panel-bg |
rgba(0,0,0,0.25) |
Settings/content panel background |
--dz-panel-text |
var(--dz-body-text) |
Settings/content panel text |
--dz-table-odd-bg |
#E2E4FF |
DataTable odd row background |
--dz-table-odd-text |
#333 |
DataTable odd row text |
--dz-table-even-bg |
#ffffff |
DataTable even row background |
--dz-table-even-text |
#333 |
DataTable even row text |
Creating a dark theme
Place variable overrides in a dark.css file in your theme folder and import it from custom.css:
custom.css:
@import url("../../css/legacy.css");
@import url("common.css");
@import url("dark.css");
dark.css:
:root {
--dz-widget-bg: #1e2a3a;
--dz-widget-text: #f0f0f0;
--dz-widget-name-bg: #2a3a50;
--dz-widget-name-border: #3a4a60;
--dz-widget-name-text: #f0f0f0;
--dz-widget-hover-bg: #253040;
--dz-widget-hover-name-bg: #354558;
--dz-status-normal: #283750;
--dz-status-protected: #354a80;
--dz-status-timeout: #8b1a23;
--dz-status-low-battery: #7a7a00;
--dz-status-disabled: #4a4a4a;
--dz-body-bg: #0f1a2a;
--dz-body-text: #f0f0f0;
--dz-input-bg: #1a2a3a;
--dz-input-text: #ddd;
--dz-input-border: #3a4a5a;
--dz-modal-bg: #1e2a3a;
--dz-modal-text: #f0f0f0;
--dz-modal-header-bg: #152030;
}
body {
background: var(--dz-body-bg);
color: var(--dz-body-text);
}
Creating a light theme
For a light theme, import common.css only — no dark.css. The --dz-* defaults in variables.css are already light-valued, so widgets render correctly without a full :root override block. You only need to override the variables you want to change.
The one variable you must override is --dz-body-bg, because its default (#202020) is intentionally dark to prevent FOUC.
custom.css:
@import url("../../css/legacy.css");
@import url("common.css");
:root {
--dz-widget-bg: #e8f0f8; /* optional: fine-tune widget colour */
--dz-body-bg: #f0f2f5; /* required: override the dark FOUC default */
--dz-body-text: #333;
}
body {
background: var(--dz-body-bg);
color: var(--dz-body-text);
}
Variable contract versioning
The --dz-* variables defined in variables.css and common.css are the stable public API for theme authors.
| Change type | Policy |
|---|---|
| Adding new variables | Safe — themes that do not reference them are unaffected |
| Changing default values | Safe with a release note — themes that override the variable are unaffected; themes relying on the default will see the change |
| Renaming or removing a variable | Requires a deprecation window. The old name is kept as an alias (--dz-old-name: var(--dz-new-name)) for at least two releases
|
| Internal CSS selectors and class names | May change between releases without compatibility guarantees — use --dz-* variables instead of targeting selectors
|
Community theme migration guide
If your theme was built before the CSS variable system was introduced, it likely contains long selector chains targeting internal widget tables. These can now be replaced with a single :root block.
Before (old approach)
/* Fighting legacy.css specificity to change widget colours */
body table#itemtablesmall tbody tr { background-color: #1e2a3a !important; }
body table#itemtabledoubleicon tbody tr { background-color: #1e2a3a !important; }
/* ... repeated 8 times for every table variant ... */
body table#itemtablesmall tbody tr td:first-child { background-color: #2a3a50 !important; }
/* ... repeated 8 times again ... */
After (new approach)
/* One :root block replaces all of the above */
:root {
--dz-widget-bg: #1e2a3a;
--dz-widget-name-bg: #2a3a50;
}
Migration checklist
- Remove all
body table#itemtable*colour overrides from your theme. - Replace them with a
:root { --dz-*: your-values; }block. - Review
!importantdeclarations on widget colour properties — most are now unnecessary. - If your theme copied rules from
legacy.css, remove those copies — they will conflict with the variable-driven rules.
Theme author guidelines
- Prefer variable overrides over selector chains. Changing
--dz-widget-bgis safer and more forward-compatible than targeting.itemortable#itemtablesmall. - Do not override Bootstrap layout globally. Overriding
.row-fluid,.span*, or.container-fluidat the top level breaks page structure. Scope overrides to specific containers. - Scope custom selectors. Target
#holder .itemrather than bare.item, to avoid interfering with modals and other contexts. - Do not use
!importanton variable-driven properties. It defeats the purpose of the variable contract and makes your overrides impossible for downstream themes to fix. - Do not import extras_and_animations.css by default unless your theme explicitly wants transitions and hover animations. When you do want them, add
@import url("extras_and_animations.css");to yourcustom.css.
Button systems
Domoticz has three separate button systems. Theme authors need to be aware of all three:
| System | Classes | Used where |
|---|---|---|
| Bootstrap 2.x | .btn, .btn-default, .btn-primary, .btn-warning, .btn-danger, .btn-info, .btn-success |
Modal footers, toolbar actions, device overview |
| Custom Domoticz | .btnstyle, .btnstylerev, .btnstyle3, .btnstyle3-sel, .btnstyle3-dis |
Switches tab, scene buttons |
| Small action | .btnsmall, .btnsmall-sel, .btn-mini |
Device options menus, log/timer tabs |
The --dz-btn-* variables control Bootstrap .btn* styles. The .btnstyle* and .btnsmall* systems do not yet have variables — override them with explicit selectors if needed.
Theme settings storage
Themes can persist their own configuration in the Domoticz database without using user variables. The settings are stored in the Preferences table under the key ThemeSettings as a single JSON object, and are included in the standard getsettings / storesettings API used by the Settings page.
How it works
When the Settings page loads, getsettings returns a ThemeSettings object in the JSON response. This object is exposed on the AngularJS scope as $scope.ThemeSettings. The currently active theme name is available on the same scope as $scope.WebTheme.
By convention, the top-level keys of ThemeSettings are theme names. This means multiple themes can store their settings in the same object without overwriting each other — switching between themes leaves every theme's settings intact.
When the user saves the Settings page, the current value of $scope.ThemeSettings is serialised and submitted as part of the normal storesettings POST. No extra API calls are required.
Reading settings in custom.js
custom.js is loaded on every page. To read settings, get the AngularJS scope from any page that has one (the main content area), then look up your theme by name:
var THEME_NAME = 'YourTheme'; // must match the folder name under www/styles/
function getThemeSettings() {
var el = document.getElementById('maindiv') || document.body;
var scope = angular.element(el).scope();
if (!scope) return {};
return (scope.ThemeSettings && scope.ThemeSettings[THEME_NAME]) || {};
}
On pages that use a different root element the scope lookup may return nothing — always guard with a null check.
Writing settings back
Update $scope.ThemeSettings[THEME_NAME] and the value will be included the next time the user saves Settings. If your theme provides its own Save button, you can also trigger the settings save directly:
var THEME_NAME = 'YourTheme';
function saveThemeSettings(newSettings) {
var el = document.getElementById('maindiv') || document.body;
var scope = angular.element(el).scope();
if (!scope) return;
scope.$apply(function () {
scope.ThemeSettings = scope.ThemeSettings || {};
scope.ThemeSettings[THEME_NAME] = newSettings;
});
}
To persist without requiring the user to click the Settings save button, call scope.StoreSettings() after updating — but only do this from within the Settings page (#Setup route), not from general page scripts.
Getting the active theme name
$scope.WebTheme holds the name of the currently active theme (the folder name under www/styles/, e.g. Nightglass). Your theme's custom.js already knows its own name, so hardcoding it is fine. $scope.WebTheme is useful if you write shared utility code that needs to be theme-agnostic.
Example: dark/light mode toggle
var THEME_NAME = 'YourTheme';
var DEFAULTS = { style: 'dark', aceTheme: 'ace/theme/tomorrow_night' };
function getSettings() {
var el = document.getElementById('maindiv') || document.body;
var scope = angular.element(el).scope();
if (!scope || !scope.ThemeSettings) return Object.assign({}, DEFAULTS);
return Object.assign({}, DEFAULTS, scope.ThemeSettings[THEME_NAME]);
}
function setStyle(style) {
var el = document.getElementById('maindiv') || document.body;
var scope = angular.element(el).scope();
if (!scope) return;
scope.$apply(function () {
scope.ThemeSettings = scope.ThemeSettings || {};
scope.ThemeSettings[THEME_NAME] = Object.assign(getSettings(), { style: style });
});
applyStyle(style);
}
Storage limits
The ThemeSettings value is stored as a text field in SQLite. There is no hard size limit enforced at the application level, but keep the JSON compact — a few kilobytes is plenty for theme configuration. Do not store binary data or per-device state here.
Accessing settings outside the Settings page
On pages other than Settings, $scope.ThemeSettings is not automatically populated. For applying settings on every page load (e.g. re-applying a dark/light mode), read the settings via a direct API call on page load:
(function () {
$.getJSON('json.htm?type=command¶m=getsettings', function (data) {
var settings = (data.ThemeSettings && data.ThemeSettings[THEME_NAME]) || DEFAULTS;
applyStyle(settings.style);
});
})();
This call is unauthenticated-safe for read-only access on the local network.
Known limitations
These items are not controlled by --dz-* variables:
- JavaScript
.css()calls — there are many$(el).css(...)calls across the frontend that apply inline styles directly to DOM elements. Inline styles have specificity(1,0,0,0)and override any stylesheet rule, including variable-driven ones. Some elements will not respond to variable overrides. extras_and_animations.css— imported only by thedefaulttheme viacustom.css. To include transitions and hover effects in your theme, add@import url("extras_and_animations.css");to yourcustom.css. The file is served fromdefault/extras_and_animations.cssvia fallback if your theme does not provide one.- Hardcoded colours in other CSS files —
css/style.css,css/bootstrap.css, andcss/demo_table_jui.csscontain many hardcoded colours that are not yet variablized. - SVG icon colours — icon-specific colours in some device SVGs may still be hardcoded and will not respond to
--dz-status-*overrides.
Browser support
CSS custom properties (var()) require:
- Chrome 49+
- Firefox 31+
- Safari 9.1+
- Edge 16+
display: contents (used on widget custom elements) had Safari accessibility issues until Safari 16. Older browser support is not a goal for the Domoticz web UI.
Directory structure
A theme lives in a subdirectory of www/styles/:
www/styles/
your-theme/
custom.css (required — theme entry point, loaded by index.html via cWebem rewrite)
common.css (optional — overrides default/common.css for this theme)
dark.css (optional — dark :root overrides, imported from custom.css)
base.css (optional — overrides default/base.css for backward compatibility)
extras_and_animations.css (optional — transitions and hover effects)
images/ (optional — theme-specific images)
fonts/ (optional — custom fonts)
Browser cache
The web frontend is configured for browser caching to speed up application launch time, which is especially useful when creating a home-screen shortcut on a mobile device.
When developing or experimenting with a theme, be sure to perform a hard reload after each change to ensure the browser fetches the latest CSS rather than serving a cached copy.
- Windows/Linux:
Ctrl+Shift+R - macOS:
Cmd+Shift+R
Because the cWebem URL rewriting is transparent to the browser, switching themes in Settings does not automatically invalidate cached copies of common.css or variables.css. A full hard reload is always required after a theme switch.