Skip to content

Latest commit

 

History

History
396 lines (317 loc) · 20.5 KB

File metadata and controls

396 lines (317 loc) · 20.5 KB

Theme Customization

The appearance of DIAL Chat is controlled by a theme: a set of colors, logos, and icons served by an external themes service and applied at runtime as CSS custom properties. Themes are deployment configuration, not a rebuild — the same application image can be rebranded by pointing it at a different themes service.

This document covers configuring a theme for a deployment, and migrating a theme from the legacy DIAL Chat. Styling of the reusable components in libs/* is a separate concern — see Styling libraries.

Configuration

Point the deployment at a themes service

Themes are served by a standalone static host, as in the legacy chat — see DIAL Chat Themes for deploying one. Configure chat-api with its base URL:

THEMES_CONFIG_URL=https://your-themes-host.example.com
THEMES_SERVICE_TIMEOUT_MS=5000

THEMES_SERVICE_TIMEOUT_MS is optional and defaults to 5000.

The frontend never talks to the themes host directly. chat-api fetches <THEMES_CONFIG_URL>/config.json and exposes it at /api/themes; image assets are proxied through /api/themes/icon?iconName=<name>, which validates the name against path traversal. Both responses are cached for 5 minutes, so a change on the themes host takes up to 5 minutes (plus that host's own cache) to appear.

When THEMES_CONFIG_URL is unset, the configuration request fails, the application logs the failure and continues with its built-in palette: the light-theme hex fallbacks compiled into tailwind.config.js. The user menu then offers no theme entries at all.

Configuration file format

{
  "themes": [
    {
      "id": "light",
      "displayName": "Light",
      "colors": {
        "bg-layer-base": "#F5F7FA",
        "text-primary": "#161B2D"
      },
      "app-logo": "logo-light.svg"
    },
    {
      "id": "dark",
      "displayName": "Dark",
      "colors": {
        "bg-layer-base": "#0C101D",
        "text-primary": "#F3F4F6"
      },
      "app-logo": "logo-dark.svg"
    }
  ],
  "images": {
    "chat-logo-light": "logo-light.svg",
    "chat-logo-dark": "logo-dark.svg",
    "chat-favicon": "favicon.png"
  }
}

Each entry in colors is applied verbatim as a CSS custom property on the <html> element: key bg-layer-base becomes --bg-layer-base. Do not write the -- prefix in the file.

Keys are not validated. An unrecognized key is set as a CSS variable that nothing reads — the theme still loads, and the misspelled color silently has no effect. A recognized key that a theme omits falls back to the built-in light value. This is the single most common way a theme "half applies".

Theme ids and the theme picker

The picker in the user menu is driven by the ids present in the configuration:

Configured ids Picker shows
light and dark Light, Dark, and System
only one of them just that one entry
neither no theme entries

System is offered only when both light and dark exist; it follows prefers-color-scheme and re-resolves when the OS setting changes. The selection is stored in the browser's local storage, so it is per user and per device.

Ids other than light, dark, and system can be present in the file, but the picker has no entry for them. They are reachable only through the overlay's theme option (see below).

Logos and icons

The application reads exactly three image fields:

Field Used for
images.chat-logo-light Header logo while the resolved theme is not dark
images.chat-logo-dark Header logo while the resolved theme is dark
images.chat-favicon Browser tab favicon (PNG, 32×32 recommended)

The remaining fields carried by the configuration contract — themes[].app-logo, images.favicon, images.default-addon, images.default-model — are accepted and ignored. Keep them in the file if the legacy chat is still served from the same themes host; do not expect them to change anything here.

Values are file names relative to the themes host; they are fetched through the /api/themes/icon proxy, so only characters allowed by its validation (alphanumerics, dash, underscore, dot) are usable.

Setting the theme from an embedding host

An embedded overlay selects the theme by id, both at construction and at runtime:

const overlay = new ChatOverlay('#chat-root', {
  domain: 'https://chat.example.com',
  theme: 'dark',
});

await overlay.setOverlayOptions({ theme: 'light' });

The id must exist in the deployment's configuration file. See the Chat Overlay Migration Guide.

Fonts are not themeable

--theme-font exists, but apps/chat/src/styles.scss declares it on html, body, and #root. A value supplied through a theme's colors map lands on <html> only and is shadowed for everything inside #root, so it has no practical effect. Changing the application font currently requires editing that stylesheet.

Color tokens

These are the CSS custom properties the application reads. Anything else in colors is inert.

Surfaces

bg-layer-sunken   bg-layer-base     bg-layer-raised
bg-backdrop       bg-error          bg-warning
bg-info           bg-success

Controls

bg-control-accent               bg-control-accent-hover
bg-control-accent-alpha         bg-control-accent-alpha-hover
bg-control-accent-alpha-active
bg-control-neutral              bg-control-neutral-default
bg-control-neutral-hover-muted  bg-control-neutral-hover-strong
bg-control-neutral-active       bg-control-inverted
bg-control-error                bg-control-error-hover
bg-control-error-active         bg-control-error-alpha-hover
bg-control-error-alpha-active
bg-control-disable-primary      bg-control-disable-secondary
bg-gradient-1                   bg-gradient-1-hover
bg-gradient-1-active            bg-gradient-2
bg-gradient-2-hover             bg-gradient-2-active

The bg-gradient-* stops paint the accent gradient behind primary buttons and the selected tab's underline.

Decorative fills

bg-visual-blue      bg-visual-green-1   bg-visual-green-2
bg-visual-brown     bg-visual-red       bg-visual-violet-1
bg-visual-violet-2

Text

text-primary       text-secondary       text-tertiary
text-accent
text-error         text-warning         text-warning-icon
text-info          text-success
text-control-permanent        text-control-inverted
text-control-accent-hover     text-control-accent-active
text-control-disable-primary  text-control-disable-secondary
text-visual-blue     text-visual-brown-1   text-visual-brown-2
text-visual-green-1  text-visual-green-2   text-visual-green-3
text-visual-red      text-visual-violet-1  text-visual-violet-2

Strokes

stroke-primary      stroke-secondary     stroke-tertiary
stroke-error        stroke-error-alpha   stroke-warning
stroke-info         stroke-success       stroke-accent
stroke-accent-alpha stroke-default       stroke-focus-black
stroke-accent-focus stroke-gradient-1    stroke-gradient-2
stroke-control-disable-primary

These name the stroke colour. Stroke width is not themable: the design scale is four fixed widths, written as plain Tailwind utilities, because borderColor already owns names like warning and error and a border-warning utility would then set a width and a colour at once.

Width Written as Used for
0.5px 0.5px solid in a stylesheet Dividers inside a table
1px border Controls, standalone dividers, table frames
1.5px DIAL_KIT_ICON_STROKE Tabler icon stroke (its own default is 2)
2px border-2 Active/selected highlighting

Tabler renders every outline icon at 2px unless told otherwise, so the icon weight has to be passed explicitly on each icon: <IconPlus stroke={DIAL_KIT_ICON_STROKE} />. Two departures are deliberate: empty-state illustrations stay lighter (stroke={1}), since 1.5px reads as a fence at 48px, and filled glyphs carry no icon stroke at all.

Shadows

shadow-xs-1  shadow-xs-2  shadow-sm  shadow-md  shadow-lg

The variables are named after the elevation that consumes them, not after their hue. shadow-xs paints two layers — a wide blue one from shadow-xs-1 and a tight grey one from shadow-xs-2. shadow-sm, shadow-md, and shadow-lg are a single blue layer each, so they take one variable apiece. Themes that set the pre-spec shadow-xs-sm-1 / shadow-xs-sm-2 need to move to the shadow-xs-* names; shadow-sm was retuned for the side panels and now has its own variable.

Besides the four scale steps there is shadow-chat-button, the resting shadow the chat button kept when shadow-sm was retuned. It is not a foundations elevation and has no variable of its own — it paints the same two variables as shadow-xs at a wider offset and blur.

There is no inset variant: a recessed seam between a panel and the content beside it is painted by the panel's own shadow-sm, not by an inset shadow on the content.

Renamed in ui-kit 0.14

The control tokens were renamed for the role they fill instead of their opacity or their literal hue. A theme that sets the old variable names keeps its colors in the Tailwind utilities — each one is still the next link in the new token's fallback chain in tailwind.config.js. The chains in libs/* are a separate mechanism and stop at the current name (see Styling libraries), so a theme still on the old names renders the hex fallback for a handful of library values. Rename them.

Pre-0.14 variable Preferred now
bg-control-disable bg-control-disable-primary
bg-control-neutral-hover bg-control-neutral-hover-muted
text-control-disable-alpha text-control-disable-primary
text-control-disable-beta text-control-disable-secondary
text-control-blue-hover / -active text-control-accent-hover / -active
bg-control-accent-gradient-from / -to bg-gradient-1 / bg-gradient-2
bg-control-accent-gradient-hover-from bg-gradient-1-hover
bg-control-accent-gradient-active-to bg-gradient-2-active
stroke-control-accent-gradient-from / -to stroke-gradient-1 / stroke-gradient-2
stroke-control-accent-gradient-hover-from / -to bg-gradient-1-hover / bg-gradient-2-hover

Two exceptions where the old name is not honored, so a theme setting it loses the color:

  • stroke-hover-alpha was removed — it held the same value as stroke-accent-alpha. Move the value there.
  • The stroke-control-accent-gradient-* stops behind the selected tab's underline have no fallback chain. Rename them as in the table above.

stroke-focus-black is unchanged as a variable; only its Tailwind class name moved (outline-focus-blackoutline-focus).

stroke-focus-bluestroke-accent-focus

The accent focus ring variable is named for the role it fills instead of its literal hue, matching the border-accent-focus / outline-accent-focus classes that consume it. The old name is still the next link in the fallback chain — in tailwind.config.js and in the libs/* chains that reference it — so a theme setting stroke-focus-blue keeps its focus-ring color. Rename it.

One caveat: @epam/ai-dial-ui-kit's own stylesheet reads --stroke-focus-blue directly, so the kit's focus rings do not pick up --stroke-accent-focus. Until the kit is updated, a theme that wants a custom accent focus ring everywhere has to set both variables.

Migrating a theme from the legacy chat

A legacy config.json loads without an error but produces mostly the built-in palette: the environment variable, the token names, and the default color scheme all changed.

Deployment configuration

Legacy New
THEMES_CONFIG_HOST THEMES_CONFIG_URL
THEMES_SERVICE_TIMEOUT_MS (optional, 5000)
/api/themes/listing, /api/themes/image/[name] /api/themes, /api/themes/icon?iconName=
Default theme: dark Default theme: light
24-hour cache on the themes host 5-minute cache in chat-api

The flipped default matters even for a faithful port: any token the theme omits now resolves to a light fallback, so a partially migrated dark theme renders as light patches rather than as approximately-dark.

Token mapping

The palette was redesigned, not renamed, so treat this as a starting point rather than a mechanical substitution. Colors carried over unchanged (same name, same role) are omitted: bg-error, bg-warning, bg-info, bg-success, stroke-primary, stroke-secondary, stroke-tertiary, stroke-error, stroke-warning, stroke-info, stroke-success, text-primary, text-secondary, text-error, text-warning, text-warning-icon, text-info, text-success.

Legacy token Closest new token
bg-layer-0bg-layer-4 bg-layer-sunken, bg-layer-base, bg-layer-raised — five elevation steps collapsed into three
bg-blackout bg-backdrop
controls-bg-accent, bg-accent-primary bg-control-accent
controls-bg-accent-hover No solid hover token; the new controls use bg-control-accent-alpha-hover / -active
controls-bg-disable, controls-bg-disable-accent bg-control-disable-primary
controls-text-permanent text-control-permanent
controls-text-disable, controls-text-primary-disable, controls-text-accent-disable text-control-disable-primary, text-control-disable-secondary — three states collapsed into two
text-accent-primary text-accent
stroke-hover stroke-accent-alpha
stroke-accent-primary stroke-accent, plus stroke-accent-focus for accent focus rings
bg-accent-secondary, bg-accent-tertiary, stroke-accent-secondary, stroke-accent-tertiary, text-accent-secondary, text-accent-tertiary No equivalent — the three-accent scheme became a single accent
bg-auth-layer-0, bg-auth-layer-1 No equivalent — the login page uses the shared surface tokens
bg-model-icon No equivalent
border-radius No equivalent — radii are Tailwind classes, not a themeable variable
codeblock-font, theme-font No equivalent in practice — see Fonts are not themeable

New token groups with no legacy counterpart — bg-visual-*, text-visual-*, bg-control-neutral*, bg-control-error*, text-control-accent-*, stroke-focus-black, stroke-error-alpha, and shadow-* — start at their built-in light values until the theme sets them. A dark theme that leaves them alone will show light chips, focus rings, and shadows.

Images

Replace themes[].app-logo with images.chat-logo-light and images.chat-logo-dark, and images.favicon with images.chat-favicon. The legacy fields are ignored rather than rejected, which is why a migrated theme can render with no logo at all and no error.

Features with no replacement

  • additional_css / ADDITIONAL_CSS_DIR. The legacy chat injected arbitrary stylesheets from a mounted directory. 1.0 replaces that with two supported channels: deployment-wide colors through the theme tokens above, and anything a token does not cover through the libs' own styling props — styles={{ colors, typography }} at the call site, described in Styling libraries. Both survive an upgrade; a stylesheet targeting generated class names does not.
  • The custom-logo UI feature flag. The logo now always comes from the theme configuration; the flag no longer exists.

Styling libraries

Components in libs/* are consumed both by this application and by external projects, so they do not depend on this theme. Each themeable value resolves through a three-tier chain — per-instance prop override, then the application theme token described above, then a hard-coded hex fallback:

background: var(--ci-bg, var(--bg-layer-sunken, #eef1f7));

Inside this application the middle tier resolves, so libs pick up the deployment theme automatically. In a project without these tokens the hex fallback applies, and a host can still override individual values through each component's styles={{ colors, typography }} prop.

The chain is exactly three tiers. The middle tier names the current token only — it does not carry the pre-0.14 aliases that tailwind.config.js keeps for its utilities, because a lib is not supposed to know this application's rename history.

The full convention — variable naming, what belongs in .module.scss versus Tailwind, the buildCssVars helper, and the checks that catch styles which silently do nothing — is in openspec/lib-styling-guide.md.