Theming

Every design value in Lovelace flows through CSS custom properties named --lovelace-*. tokens.css defines them. Component styles only reference them through var(). To change how the components look, you re-point tokens; you do not fight component CSS.

To experiment before you read further, jump to the theme playground.

The three token tiers

Tokens form a chain of three tiers. Each tier references the one below it.

TierExampleRole
Component--lovelace-button-primary-backgroundThe value one component surface uses.
Semantic--lovelace-color-background-accentA role-based value. Semantic tokens change between light and dark themes.
Primitive--lovelace-color-accent-500A raw palette value. Primitives never change with theme.

For example, the primary button background resolves as:

--lovelace-button-primary-background
→ var(--lovelace-color-background-accent)
→ var(--lovelace-color-accent-500) /* #4e53d1 */

Pick your tier by blast radius. Override a semantic token to restyle every surface that shares the role. Override a component token to change one surface only. Primitive tokens are not overridable through the provider; they exist to feed the tiers above.

Override tokens per brand

Pass the override prop to LovelaceProvider. Keys are the overridable semantic and component tokens, typed as LovelaceOverridableToken. Values are CSS values.

1import { LovelaceProvider } from "@ada-cx/lovelace";
2import { t } from "@ada-cx/lovelace/tokens";
3
4<LovelaceProvider
5 override={{
6 "color-background-accent": "#0f62fe",
7 "font-size-body": t("font-size-14"),
8 }}
9>
10 {children}
11</LovelaceProvider>;

Token keys drop the --lovelace- prefix. The provider applies each override as an inline CSS variable on its root element, and the value cascades to every Lovelace component inside.

TypeScript rejects unknown keys and primitive-token keys at compile time.

The tokens module

@ada-cx/lovelace/tokens exports typed helpers for working with tokens:

  • t(name) returns a var() reference, for example t("font-size-14") returns "var(--lovelace-font-size-14)". Use it to point one token at another.
  • tokenName(name) returns the bare custom property name, for example "--lovelace-font-size-14".
  • LovelaceToken, LovelaceOverridableToken, and TokenRef are the token name and reference types.

How Ada applies your brand settings

The Ada widget does not ship per-brand CSS. It maps the brand settings from the Ada dashboard, under Settings > Chat window, to a token override map, and passes that map to LovelaceProvider. The mapping is a public export named createBrandOverrides. A custom UI that calls it applies your brand exactly the way the widget does.

1import {
2 createBrandOverrides,
3 LovelaceProvider,
4 useResolvedTheme,
5} from "@ada-cx/lovelace";
6
7function BrandedChat({ children }) {
8 const scheme = useResolvedTheme("auto"); // "light" | "dark"
9
10 return (
11 <LovelaceProvider
12 theme="auto"
13 override={createBrandOverrides(
14 {
15 tintColor: "#0f62fe",
16 headerColorEnabled: true,
17 headerColor: "#0b3d91",
18 cornerStyle: "rectangular",
19 },
20 scheme,
21 )}
22 >
23 {children}
24 </LovelaceProvider>
25 );
26}

The second argument is the resolved color scheme, not the configured theme. Resolve "auto" through useResolvedTheme first (see Light and dark themes). The mapping decides whether your accent color can serve as text, and that decision depends on the surface the text lands on.

The settings object

Every BrandSettings field is optional. An absent or null field contributes no overrides, so createBrandOverrides({}, scheme) returns {}.

FieldTypeDashboard setting
advancedColorsEnabledbooleanAdvanced colors. Gates textOverAccentColor and headerTextColor.
cornerStyle"round" | "rectangular"Chat style. Defaults to "round".
headerColorstringHeader background color.
headerColorEnabledbooleanHeader color toggle. Gates headerColor and headerTextColor.
headerTextColorstringHeader text color.
textOverAccentColorstringText over accent color.
textSize"default" | "small" | "large"Text size. Defaults to "default".
tintColorstringAccent color.

The types BrandCornerStyle, BrandOverrides, BrandSettings, and BrandTextSize ship with the package.

The mapping

createBrandOverrides applies these rules, in order:

  1. cornerStyle: "rectangular" re-points the message, composer, media preview, and container radius tokens at the flat border-radius-10 primitive. textSize shifts the caption, body, and title font-size and line-height tokens one step down ("small") or up ("large"). The defaults contribute nothing.
  2. tintColor paints color-background-accent unconditionally. Text on the accent fill, color-content-on-accent, becomes black or white, whichever contrasts more with the tint.
  3. The tint also becomes standalone accent text, color-content-accent, but only when it reads against the rendered scheme’s surface. A tint close to the background would make labels invisible, so the tint must clear a 3:1 contrast floor, or at least read no worse than the default value it would replace.
  4. With advancedColorsEnabled, textOverAccentColor replaces the derived on-accent text color verbatim.
  5. With headerColorEnabled, headerColor paints header-background. The header text is headerTextColor (advanced colors only), repaired to black or white when it measurably falls below the WCAG AA 4.5:1 contrast floor against the header background. Without a configured header text color, the mapping derives the maximum-contrast black or white from the background.

The legibility guards measure hex colors only. Values the parser cannot read, such as named colors or rgba() values, pass through unrepaired where they are kept at all. Repairing an unmeasurable value would trade one unknown rendering for another.

Colorimetry helpers

The contrast measurements behind the mapping are public too:

  • contrastRatioBetween(a, b) returns the WCAG contrast ratio between two hex colors, or null when either cannot be parsed.
  • isLegibleAgainst(color, surface, fallback) reports whether a color is safe to use as accent text on a given surface.
  • isLegibleOnSurface(color, scheme) runs the same check against the default Lovelace surface for a scheme.
  • LOVELACE_SURFACES holds the default light and dark surface colors the checks measure against.
  • readableTextOnAccent(accent, fallback) picks black or white for text painted on an accent fill.

Theme playground

Try your brand on real Lovelace components. The playground runs createBrandOverrides, the same function the Ada widget uses, so the preview matches what the widget renders.

Six presets seed the controls: Ada, Ocean, Sunset, Terminal, Bubblegum, and Mono noir. Each preset is a complete combination of accent, theme, corner style, text size, and header colors. Changing any control clears the active preset and keeps your values.

The interactive theme playground could not load in this view. The sections above describe the same token system and the same brand mapping.

Every control corresponds to a dashboard setting under Settings > Chat window:

Playground controlDashboard settingWhat it drives
Accent colorAccent colorcolor-background-accent, plus the derived on-accent and accent text colors
ThemeThemeThe rendered scheme; auto follows your OS preference
Chat styleChat styleThe corner radius tokens; rectangular flattens them
Text sizeText sizeThe four-step type scale
Advanced colors, text over accentAdvanced colors, Text over accent colorcolor-content-on-accent, applied verbatim
Header color, header textHeader color, Header text colorheader-background and header-content, with the 4.5:1 repair

Two copy buttons export your result: one copies the values to enter in the dashboard, and one copies a runnable createBrandOverrides call for a custom UI. The live component gallery follows the playground’s active settings.

How @layer precedence works

tokens.css wraps every token definition in @layer lovelace.tokens. Layered CSS always loses to unlayered CSS, regardless of specificity or source order.

This is a deliberate escape hatch for your own stylesheets:

  • Any unlayered --lovelace-* rule you write wins over the shipped token defaults. A plain :root { --lovelace-color-background-accent: #0f62fe; } in your CSS is enough.
  • Provider override values win the same way, because inline styles beat all stylesheet declarations.

Component styles in style.css are unlayered, hashed classes. Do not target those class names; they change between versions. Extend components through className, style, tokens, and the data-* state attributes instead.

If you need tokens without component styles, for example to build a custom theme or to use the palette in your own components, import @ada-cx/lovelace/tokens.css alone.

Light and dark themes

LovelaceProvider takes a theme prop: "light", "dark", or "auto" (the default).

1<LovelaceProvider theme="dark">{children}</LovelaceProvider>
  • The resolved scheme is stamped as data-theme="light" or data-theme="dark" on the provider’s wrapper element.
  • Semantic color tokens are re-declared under each data-theme value, so every component inside re-resolves its colors automatically.
  • auto follows the OS prefers-color-scheme setting and updates live when the user changes it.

To read the resolved scheme in your own code, use the useResolvedTheme hook:

1import { useResolvedTheme } from "@ada-cx/lovelace";
2
3const resolved = useResolvedTheme("auto"); // "light" | "dark", updates live

Use this hook instead of your own matchMedia query. It resolves through the same code path that stamps data-theme, so your logic and the rendered theme can never disagree.

Theme-dependent color overrides apply in both themes, because inline overrides sit above the data-theme re-declarations. If an override needs a different value per theme, read useResolvedTheme and pass a different override map for each scheme.