Themes
Kittox ships with a built-in theme system that controls the global look-and-feel of an application (colors, fonts, icons, light/dark mode). Everything is configured from a single Theme: node in Config.yaml.
Overview
The Theme node has the mode as its value (Light, Dark or Auto) and optional sub-nodes for fine-grained customization:
Theme: Auto
Primary-Color: SteelBlue
Font-Family: Segoe UI
Font-Size: 13px
IconStyle: outlined
IconSize: MediumIf the Theme node is not specified at all, Kittox uses Auto with the default palette, system font and filled icons at Medium size.
| Sub-node | Default | Description |
|---|---|---|
| (value) | Auto | Theme mode: Light, Dark, or Auto (follows the OS preference via prefers-color-scheme) |
Primary-Color | (none) | Base accent color — any CSS color name or #rrggbb value. Drives the toolbar, status bar and accent palette via CSS color-mix() |
Font-Family | system UI stack | Font family used by the whole application |
Font-Size | 13px | Base font size (any CSS size unit) |
IconStyle | filled | Material Design Icon style: filled, outlined, round, sharp, two-tone |
IconSize | Medium | Default icon size: Small (1em), Medium (1.4em), Large (1.8em) |
Theme mode
The value of the Theme node selects the base palette:
| Mode | Behavior |
|---|---|
Light | Forces the light palette. Rendered as <html data-theme="light"> |
Dark | Forces the dark palette. Rendered as <html data-theme="dark"> |
Auto | No data-theme attribute is emitted; CSS @media (prefers-color-scheme: dark) decides at runtime based on the user's OS setting |
# Always light
Theme: Light
# Always dark
Theme: Dark
# Follow the OS (default)
Theme: AutoTIP
Auto is the recommended default — it respects each user's OS/browser preference and switches dynamically when the user toggles dark mode without reloading the page.
Primary-Color
Setting Primary-Color generates a full chrome and accent palette from a single color. Internally, Kittox uses CSS color-mix() to derive:
- toolbar and panel header background (
--kx-chrome) - darker variant for status bar and hover states (
--kx-chrome-dark) - lighter variants for splitters and group headers (
--kx-chrome-light,--kx-chrome-mid) - accent color for focus rings, selected rows, active buttons (
--kx-accent,--kx-accent-bg,--kx-accent-ring) - chrome text color — automatically chosen between light and dark based on the perceived luminance (ITU-R BT.601) of the primary color
Both theme-name values (FireBrick, SteelBlue, Green…) and hex values (#2c3e50) are supported.
# Ethea red
Theme: Light
Primary-Color: FireBrick
# Corporate blue, dark mode
Theme: Dark
Primary-Color: "#1e3a8a"
# Auto mode with a neutral accent
Theme: Auto
Primary-Color: TealTIP
In Light mode the page text colors are also derived from Primary-Color (dark shades), giving a cohesive monochromatic look. In Dark mode only the chrome/accent overrides apply — page text stays on the default dark palette to preserve readability.
Font-Family and Font-Size
Font-Family and Font-Size override the CSS variables --kx-font and --kx-font-size on :root. They affect the whole application — forms, grids, toolbars, menus.
Theme: Light
Font-Family: Inter
Font-Size: 14pxTheme: Auto
Font-Family: Comic Sans MS
Font-Size: 12pxWhen omitted, Kittox uses the platform's native UI font stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif.
IconStyle
Kittox icons are Material Design Icons rendered via CSS mask-image. IconStyle picks one of the five Material variants — available for every icon:
| Value | Description |
|---|---|
filled (default) | Solid/filled glyphs |
outlined | Outline-only glyphs, thinner look |
round | Rounded corner variant |
sharp | Angular, geometric variant |
two-tone | Two-tone glyphs with primary and secondary shades |
Theme: Light
IconStyle: outlinedBecause icons use CSS mask-image, their color follows the surrounding text color and automatically adapts to the active theme — no per-theme icon sets needed.
See Labels and Icons for the complete icon name map and per-view overrides.
IconSize
Sets the default icon size throughout the application:
| Value | Size |
|---|---|
Small | 1em |
Medium (default) | 1.4em |
Large | 1.8em |
Theme: Light
IconSize: SmallIndividual icons can still override the default with the CSS classes kx-icon-sm, kx-icon-md, kx-icon-lg.
User-selectable theme
Theme: Auto is the recommended default precisely because it lets each user see the app in their preferred OS palette. Kittox can also expose a small three-button toggle (Light / Auto / Dark) to the end user, with the choice persisted in the browser. Two ingredients:
Opt-in in
Config.yaml— addUserSelection: TrueunderTheme: Auto:yamlTheme: Auto UserSelection: True # default False Primary-Color: SteelBlueThe flag is honoured only when
Theme: Auto. WithTheme: LightorTheme: Darkthe admin has fixed the palette and the switcher is suppressed — a server-side decision must not be silently overridden by a client-side widget.Place the switcher in any view via the
ThemeSwitchercontroller — typical spots are the login page and a topbar of the home view:yaml# LoginView.yaml — switcher below the form Controller: BorderPanel: NorthView: Controller: HtmlPanel Height: 110 Html: ... SouthView: Controller: ThemeSwitcheryaml# Home.yaml — switcher in a thin topbar Controller: BorderPanel NorthView: Height: 44 Controller: ThemeSwitcher CenterController: TabPanel ...The controller renders three small icon-buttons (☀ / ⊙ / 🌙). When
UserSelection: Trueis not in effect the controller emits nothing — the container collapses (or, for fixed-height regions, shows empty). Drop it in defensively; it stays invisible until the admin opts in.
How it works under the hood
| Where | What happens |
|---|---|
Server (<head>) | When Theme: Auto + UserSelection: True, Kittox emits a tiny inline script in <head> that runs synchronously before CSS paints, reads localStorage['kx_theme:<AppName>'] and sets <html data-theme="..."> accordingly. This anti-FOUC step is what guarantees no theme flash on first paint or reload. |
Client (kxtheme.js) | Public API: kxTheme.set(appName, mode) writes localStorage and updates data-theme. kxTheme.get(appName) returns the saved value (or 'auto'). A kx-theme-changed event is dispatched on window so any other widget can react. |
| Persistence | Key is kx_theme:<AppName> — per-app scope. Two Kittox applications running in the same browser keep independent preferences. Value is 'light', 'dark', or removed ('auto' = follow OS). |
| Switch from "Auto" | When the user picks Auto, the JS removes data-theme from <html>; the CSS @media (prefers-color-scheme: dark) rule takes over again. |
Persistence scope
Persistence is per browser + per app (localStorage), not per server-side session — the choice survives logout, login as a different user, JWT expiry, and re-issue cookies. It is browser-private: clearing browser data resets it. To enforce a fixed palette regardless of user choice, set Theme: Light (or Dark) explicitly — the switcher disappears even if dropped in a view.
Live transitions
Switching theme is instant — no page reload. The CSS rules cascade on [data-theme] and the existing CSS custom-properties palette swaps over without any JavaScript orchestration beyond the attribute change. HTMX swaps that arrive after a theme change keep the chosen theme because data-theme lives on <html>, outside any HTMX target.
Charts and live theme refresh
Chart.js renders text on a <canvas> and reads Chart.defaults.color (a JS value) at the moment each chart instance is created — it does not re-read from CSS on every paint. Kittox integrates so that charts:
Match the active theme at creation time: before instantiating a chart,
kxChart.initprobesvar(--kx-tree-leaf-text)via a temporary DOM element and readsgetComputedStyle(probe).color. The probe forces the browser to resolve any functional CSS color notation (color-mix(),oklch(), …) into a plainrgb()string, which is the only form Chart.js's internal color parser understands.React to the user's switch live:
kxtheme.jsdispatches akx-theme-changedevent onwindowwhenever the user clicks the ThemeSwitcher.kxChartlistens for it, re-resolves the text color in arequestAnimationFrame(giving the browser one frame to apply the newdata-themeattribute), then writes the colour explicitly into every active chart instance —chart.options.color,plugins.legend.labels.color,plugins.title.color,scales.*.ticks.color,scales.*.title.color— and callschart.update('none'). Without this, only freshly created charts pick up the new theme; existing ones would stay frozen on the colour cached at theirnew Chart()time.
Cascade specificity in dark mode
The server emits a <style> block in <head> that derives the chrome palette from Primary-Color via color-mix(). With Primary-Color: Green and Theme: Auto, the server's :root rule lands on the same specificity (0,1,0) as the CSS file's default [data-theme="dark"] selector — and wins by source order, leaking the green-derived light-mode colours into dark mode. Two defences are in place:
| Layer | Defence |
|---|---|
kittox.css | The dark theme block uses the type-qualified selector html[data-theme="dark"] (specificity 0,1,1), so it wins over the server's :root (0,1,0) regardless of source order. |
Server <style> | The dark block also emits explicit neutral resets for --kx-text, --kx-text-secondary, --kx-text-muted, --kx-tree-folder-text, --kx-tree-leaf-text at html[data-theme="dark"] (0,1,1), beating the server's own :root (0,1,0). This makes the fix robust even if the browser still has the previous version of kittox.css cached. |
--kx-accent-bg is similarly split: light theme keeps color-mix(Primary, white 85%) (pastel on a light surface), dark theme switches to color-mix(Primary, transparent 88%) (translucent over a dark surface). The two values are emitted in their respective scopes, not in the shared chrome block.
Real-world examples
HelloKitto — branded red + custom font

From Examples/HelloKitto/Home/Metadata/Config.yaml:
Theme: Light
Primary-Color: FireBrick
Font-Family: Comic Sans MS
Font-Size: 12px
IconStyle: filledProduces a playful, strongly-branded UI with red chrome/accent and a casual font.
KEmployee — minimalist outlined light

From Examples/KEmployee/Home/Metadata/Config.yaml:
Theme: Light
IconStyle: outlinedDefaults for everything else — clean corporate look.
TasKitto — dark mode with small two-tone icons

From Examples/TasKitto/Home/Metadata/Config.yaml:
Theme: Dark
Font-Size: 12px
IconStyle: two-tone
IconSize: mediumA compact dark UI; two-tone icons provide gentle color cues against the dark chrome.
CSS custom properties
Every theme value is applied through a CSS variable on :root (light / auto) or [data-theme="dark"] (dark). The most relevant ones:
| Variable | Role |
|---|---|
--kx-font, --kx-font-size | Base font family and size |
--kx-bg | Page background |
--kx-surface, --kx-surface-alt | Panel and card surfaces |
--kx-text, --kx-text-secondary, --kx-text-muted | Primary text tones |
--kx-chrome, --kx-chrome-dark, --kx-chrome-hover, --kx-chrome-mid, --kx-chrome-light | Toolbar / header palette |
--kx-chrome-text, --kx-chrome-btn-hover | Text and button hover on chrome |
--kx-accent, --kx-accent-bg, --kx-accent-ring | Focus rings, selected rows, active buttons |
--kx-status-bg, --kx-status-text, --kx-status-border | Status bar |
--kx-input-bg, --kx-input-border | Form input fields |
--kx-border, --kx-border-light | Separators and rules |
--kx-error, --kx-error-surface, --kx-error-border | Validation / danger colors |
--kx-overlay, --kx-shadow | Modal overlay and drop shadows |
The complete list lives in Home/Resources/css/kittox.css. To override individual variables (without re-declaring the whole theme), see CSS Theming — it covers application.css, the custom-stylesheet file that is picked up automatically by every Kittox application.
See also
- Config File — full reference of the
Config.yamltop-level nodes - CSS Theming — customizing individual CSS rules in
application.css - Labels and Icons — Material Design icon name map and per-view overrides
- KIDE Config Editor — edit
Config.yamlvisually in KIDEx
