How to theme Domoticz: Difference between revisions

From Domoticz Wiki
Jump to navigation Jump to search
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-03-29<br>
'''Revision:''' 2026-04-23<br>
'''Minimum build:''' 17675


== 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&param=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, and legacy.css are served from default/ unless your theme provides its own copies.
  • Fallback is per-file — you can override dark.css alone 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 :root blocks have the same specificity (0,1,0)last one wins by source order.
  • variables.css :root loads first → dark.css :root overrides it → theme's inline :root overrides 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

  1. Remove all body table#itemtable* colour overrides from your theme.
  2. Replace them with a :root { --dz-*: your-values; } block.
  3. Review !important declarations on widget colour properties — most are now unnecessary.
  4. 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-bg is safer and more forward-compatible than targeting .item or table#itemtablesmall.
  • Do not override Bootstrap layout globally. Overriding .row-fluid, .span*, or .container-fluid at the top level breaks page structure. Scope overrides to specific containers.
  • Scope custom selectors. Target #holder .item rather than bare .item, to avoid interfering with modals and other contexts.
  • Do not use !important on 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 your custom.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&param=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 the default theme via custom.css. To include transitions and hover effects in your theme, add @import url("extras_and_animations.css"); to your custom.css. The file is served from default/extras_and_animations.css via fallback if your theme does not provide one.
  • Hardcoded colours in other CSS filescss/style.css, css/bootstrap.css, and css/demo_table_jui.css contain 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.