> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ada.cx/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ada.cx/_mcp/server.

# 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](#theme-playground).

## The three token tiers

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

| Tier      | Example                                | Role                                                                      |
| --------- | -------------------------------------- | ------------------------------------------------------------------------- |
| Component | `--lovelace-button-primary-background` | The value one component surface uses.                                     |
| Semantic  | `--lovelace-color-background-accent`   | A role-based value. Semantic tokens change between light and dark themes. |
| Primitive | `--lovelace-color-accent-500`          | A 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.

```tsx
import { LovelaceProvider } from "@ada-cx/lovelace";
import { t } from "@ada-cx/lovelace/tokens";

<LovelaceProvider
  override={{
    "color-background-accent": "#0f62fe",
    "font-size-body": t("font-size-14"),
  }}
>
  {children}
</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.

```tsx
import {
  createBrandOverrides,
  LovelaceProvider,
  useResolvedTheme,
} from "@ada-cx/lovelace";

function BrandedChat({ children }) {
  const scheme = useResolvedTheme("auto"); // "light" | "dark"

  return (
    <LovelaceProvider
      theme="auto"
      override={createBrandOverrides(
        {
          tintColor: "#0f62fe",
          headerColorEnabled: true,
          headerColor: "#0b3d91",
          cornerStyle: "rectangular",
        },
        scheme,
      )}
    >
      {children}
    </LovelaceProvider>
  );
}
```

The second argument is the resolved color scheme, not the configured theme. Resolve `"auto"` through `useResolvedTheme` first (see [Light and dark themes](#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 `{}`.

| Field                   | Type                                  | Dashboard setting                                                   |
| ----------------------- | ------------------------------------- | ------------------------------------------------------------------- |
| `advancedColorsEnabled` | `boolean`                             | Advanced colors. Gates `textOverAccentColor` and `headerTextColor`. |
| `cornerStyle`           | `"round"` \| `"rectangular"`          | Chat style. Defaults to `"round"`.                                  |
| `headerColor`           | `string`                              | Header background color.                                            |
| `headerColorEnabled`    | `boolean`                             | Header color toggle. Gates `headerColor` and `headerTextColor`.     |
| `headerTextColor`       | `string`                              | Header text color.                                                  |
| `textOverAccentColor`   | `string`                              | Text over accent color.                                             |
| `textSize`              | `"default"` \| `"small"` \| `"large"` | Text size. Defaults to `"default"`.                                 |
| `tintColor`             | `string`                              | Accent 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 10px `border-radius-10` semantic token. `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.

> **Note**
>
> 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`](#how-ada-applies-your-brand-settings), 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 control                | Dashboard setting                       | What it drives                                                               |
| --------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------- |
| Accent color                      | Accent color                            | `color-background-accent`, plus the derived on-accent and accent text colors |
| Theme                             | Theme                                   | The rendered scheme; `auto` follows your OS preference                       |
| Chat style                        | Chat style                              | The corner radius tokens; `rectangular` flattens them                        |
| Text size                         | Text size                               | The four-step type scale                                                     |
| Advanced colors, text over accent | Advanced colors, Text over accent color | `color-content-on-accent`, applied verbatim                                  |
| Header color, header text         | Header color, Header text color         | `header-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 previews in the [component catalog](/lovelace/components) follow 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:

* Unlayered `--lovelace-*` rules override shipped token defaults on the same element. Override themed colors and component tokens on `[data-theme]`, where their defaults are declared. For example, use `[data-theme] { --lovelace-color-background-accent: #0f62fe; }`. Root overrides still reach tokens that the provider does not redeclare.
* 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).

```tsx
<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:

```tsx
import { useResolvedTheme } from "@ada-cx/lovelace";

const 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.

> **Note**
>
> 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.