From 3efd2cdd1fab118f6ec9cf859a765873dcb23c89 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:22:05 +0200 Subject: [PATCH 1/4] docs: document interface.theme, appearance scales and the ClickHouse theme Add a Theme page under the librechat.yaml object structure covering bundled theme names, the inline definition format, theme precedence, shared-link tenant themes, every appearance key with its default, brand tokens and the ClickHouse reference theme. Add the theme field to the interface page and document the REACT_APP_THEME_* build-time colors in the environment variables page. --- content/docs/configuration/dotenv.mdx | 20 ++ .../object_structure/interface.mdx | 32 +++ .../librechat_yaml/object_structure/meta.json | 1 + .../librechat_yaml/object_structure/theme.mdx | 192 ++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 content/docs/configuration/librechat_yaml/object_structure/theme.mdx diff --git a/content/docs/configuration/dotenv.mdx b/content/docs/configuration/dotenv.mdx index ee2a19c5c..5c15db06b 100644 --- a/content/docs/configuration/dotenv.mdx +++ b/content/docs/configuration/dotenv.mdx @@ -3549,6 +3549,26 @@ Properly setting cache headers is crucial for optimizing the performance and eff > **Markdown example:** `CUSTOM_FOOTER=[Link 1](http://example1.com) | [Link 2](http://example2.com)` +#### Theme Colors + +', + 'string', + 'Overrides one theme color token at build time. The value is a space-separated `R G B` triplet.', + '# REACT_APP_THEME_SURFACE_SUBMIT=4 120 87', + ], + ]} +/> + +**Behaviour:** + +- Every color token in the theme engine can be set this way: drop the `rgb-` prefix from the token name and upper-snake-case the rest, so `rgb-surface-submit` becomes `REACT_APP_THEME_SURFACE_SUBMIT` and `rgb-status-error-border` becomes `REACT_APP_THEME_STATUS_ERROR_BORDER`. The token list is the `IThemeRGB` interface in [`packages/client/src/theme/types/index.ts`](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/client/src/theme/types/index.ts). +- Values are inlined into the client when it is built, so the client has to be rebuilt after changing them. Setting them on a prebuilt Docker image has no effect. +- The same colors apply in light and dark mode, and they only change colors, not shape. +- [`interface.theme`](/docs/configuration/librechat_yaml/object_structure/theme) in `librechat.yaml` outranks these colors, and the high-contrast modes outrank both. `interface.theme` also needs no rebuild and can change shape, so prefer it for new deployments. + #### Birthday Hat + +**Default:** _None (the default LibreChat theme, or the user's own theme)_ + +**Example:** + +```yaml filename="interface / theme" +interface: + theme: clickhouse +``` + +See [Theme](/docs/configuration/librechat_yaml/object_structure/theme) for the inline definition format, every appearance key with its default, the ClickHouse reference theme and how shared links pick their theme. + ## mcpServers > **Deprecated for permission management.** The `use`, `create`, `share`, and `public` sub-keys seed role permissions at startup. Prefer the [Admin Panel](/docs/features/admin_panel) for managing MCP server permissions per role/group/user. The `placeholder` and `trustCheckbox` sub-keys are unaffected. diff --git a/content/docs/configuration/librechat_yaml/object_structure/meta.json b/content/docs/configuration/librechat_yaml/object_structure/meta.json index aa15aff93..bfbb6e919 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/meta.json +++ b/content/docs/configuration/librechat_yaml/object_structure/meta.json @@ -5,6 +5,7 @@ "---General---", "config", "interface", + "theme", "registration", "turnstile", "---Models & Specs---", diff --git a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx new file mode 100644 index 000000000..f7290dd26 --- /dev/null +++ b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx @@ -0,0 +1,192 @@ +--- +title: Theme +icon: Palette +description: Apply a bundled or custom theme to every user with interface.theme in librechat.yaml +--- + +## Overview + +`interface.theme` sets the deployment theme: the colors, shape, typography, shadows and motion that every user sees, in both light and dark mode. It takes either the name of a theme bundled with LibreChat or an inline theme definition. + + + `interface.theme` and the appearance scales described on this page are on LibreChat's `canary` + branch and are not part of a tagged release yet. + + +```yaml filename="interface / theme" +interface: + theme: clickhouse +``` + +When `interface.theme` is unset, LibreChat behaves as before: users see the default LibreChat theme, or whatever build-time colors or stored theme apply to them. + +This page covers what an operator sets in `librechat.yaml`. The theme engine itself, including the full token list and how each token maps to Tailwind utilities, is documented in the [theme README](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/client/src/theme/README.md) in the LibreChat repository. + +## How the theme is chosen + +The client picks one theme, highest priority first: + +1. **High-contrast modes.** A user who picks `high-contrast-light` or `high-contrast-dark`, or whose system setting resolves to high contrast, always gets the built-in accessible palette. +2. **`interface.theme`** from `librechat.yaml`. +3. **`REACT_APP_THEME_*`** build-time colors. See [Theme Colors](/docs/configuration/dotenv#theme-colors). +4. **The user's stored theme** in their browser. + +The deployment theme only decides colors and shape. Each user still chooses light, dark or system mode themselves. + +The deployment theme is never written to the user's browser storage. If you remove `interface.theme`, users get their own stored theme back (or the `REACT_APP_THEME_*` colors, when the build sets them). + +The theme is part of the pre-login configuration, so the login and registration pages already render with it. + +### Shared links + +A shared link paints the theme of the tenant that owns the link, not the theme of the person viewing it. If the owning tenant sets no `interface.theme`, or its shared-link configuration fails to load, the shared page shows no deployment theme rather than falling back to the viewer's. Leaving the shared page restores the viewer's theme. + +## Bundled themes + +| Name | Theme | +| --- | --- | +| `librechat` | The default LibreChat palette and shape. Setting it explicitly also overrides `REACT_APP_THEME_*` colors and users' stored themes. | +| `clickhouse` | A reference theme built from ClickHouse's Click UI design tokens. See [ClickHouse theme](#clickhouse-theme). | + +A name that is not one of these is ignored: the browser console logs `[DeploymentTheme] Ignoring unknown interface.theme ""` and the app falls back to the next source in the list above. + +### ClickHouse theme + +`interface.theme: clickhouse` changes both color and shape: + +- **Palette.** Every color token is defined in both modes from Click UI's light and dark tokens, so nothing falls back to the LibreChat palette. A few values are moved along their Click UI ramps where the verbatim value missed WCAG AA contrast. +- **Accent.** Near-black `#151515` in light mode and the ClickHouse yellow `#faff69` in dark mode, used for the accent, submit button and focus ring. +- **Radii.** A tighter scale taken from Click UI's `border.radii`: controls and `sm` through `lg` at `0.25rem`, surfaces and `xl`/`2xl` at `0.5rem`, large surfaces and `3xl` at `0.75rem`. +- **Shadows.** Click UI's single elevation shadow on every raised surface, at 0.15 opacity in light mode and 0.6 in dark, with a hairline shadow for the `xs` and `sm` steps. +- **Fonts.** The UI font stays Inter. Code uses **Inconsolata**, followed by the same system monospace fallbacks as the default theme. + + + LibreChat only ships `@font-face` rules for Inter and Roboto Mono (see `client/src/style.css`). + Inconsolata renders only on machines that have it installed, or if your deployment serves it + itself, for example by adding an `@font-face` rule to a custom client build. Otherwise code + blocks fall back to `ui-monospace`, SF Mono, Menlo, Cascadia Mono, Liberation Mono or Consolas, + whichever is available. + + +## Inline theme definition + +Instead of a name, `interface.theme` can hold a theme definition. Anything you leave out falls back to LibreChat's defaults for that mode, so a definition only needs the values you want to change. + +```yaml filename="interface / theme" +interface: + theme: + version: 1 + name: acme + modes: + light: + colors: + rgb-accent-primary: '29 78 216' + rgb-accent-primary-hover: '30 64 175' + rgb-ring-primary: '29 78 216' + rgb-surface-submit: '29 78 216' + rgb-surface-submit-hover: '30 64 175' + rgb-link: '29 78 216' + appearance: + controlRadius: '0.375rem' + radiusLg: '0.375rem' + radiusXl: '0.5rem' + shadowLg: '0 8px 16px -4px rgb(0 0 0 / 0.2)' + dark: + colors: + rgb-accent-primary: '96 165 250' + rgb-accent-primary-hover: '147 197 253' + rgb-ring-primary: '96 165 250' + rgb-surface-submit: '37 99 235' + rgb-surface-submit-hover: '29 78 216' + rgb-link: '96 165 250' + appearance: + controlRadius: '0.375rem' + radiusLg: '0.375rem' + radiusXl: '0.5rem' + shadowLg: '0 8px 16px -4px rgb(0 0 0 / 0.5)' + brands: + provider-openai: '#10a37f' +``` + +| Key | Type | Description | +| --- | --- | --- | +| `version` | Number | Required. Must be `1`. | +| `name` | String | Required. A non-empty name for the theme. | +| `modes` | Object | Required. Holds `light` and/or `dark`. A mode you leave out uses LibreChat's defaults for that mode. | +| `modes..colors` | Object | Color tokens for that mode. Keys are `rgb-*` token names, values are `R G B` triplets. See [Colors](#colors). | +| `modes..appearance` | Object | Shape, typography, shadow and motion values for that mode. See [Appearance](#appearance). | +| `modes..brands` | Object | Provider brand colors for that mode. Overrides the theme-wide `brands`. | +| `brands` | Object | Provider brand colors for both modes. See [Brands](#brands). | + +### Validation + +A definition is checked twice: + +- **When `librechat.yaml` is loaded.** The shape must match: `version: 1`, a `name`, only `light` and `dark` under `modes`, only `colors`, `appearance` and `brands` inside a mode, color keys starting with `rgb-` and color values written as three space-separated numbers. A mismatch fails [configuration validation](/docs/configuration/dotenv#configuration-validation). +- **In the browser.** Token names and values are checked against the theme registry: unknown color, appearance or brand tokens, RGB channels above 255, and values of the wrong kind are rejected. If any check fails, the **whole** definition is ignored, the browser console logs `[DeploymentTheme] Ignoring invalid interface.theme: ...` with every error, and the app falls back to the next theme source. + +### Colors + +Colors use the same token names as the theme engine: `rgb-` followed by the token, such as `rgb-surface-primary`, `rgb-text-primary`, `rgb-border-medium`, `rgb-accent-primary` or `rgb-status-error-subtle`. The full list of 93 tokens is the `IThemeRGB` interface in [`packages/client/src/theme/types/index.ts`](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/client/src/theme/types/index.ts). + +Each value is a bare `R G B` triplet with every channel from `0` to `255`, for example `'255 255 255'`. Hex values and `rgb(...)` are rejected. Quote the value so YAML reads it as a string. + +A few tokens follow a related token you did set when you leave them out, so a partial palette stays coherent. For example, `rgb-text-muted` follows `rgb-text-tertiary`, and `rgb-surface-composer-hover` follows `rgb-surface-hover`. + +### Appearance + +Appearance values are set **per mode**, and a mode without them uses the defaults below. To change shape in both modes, repeat the values under `light` and `dark`, as in the example above. + +| Key | Controls | Default | +| --- | --- | --- | +| `controlRadius` | Corner radius of controls such as buttons and inputs | `0.75rem` | +| `roundControlRadius` | Radius of fully rounded controls | `9999px` | +| `surfaceRadius` | Radius of surfaces such as cards and menus | `1rem` | +| `largeSurfaceRadius` | Radius of large surfaces such as dialogs | `1.5rem` | +| `radiusSm` | The `rounded-sm` step used across the app | `calc(0.5rem - 4px)` | +| `radiusMd` | The `rounded-md` step | `calc(0.5rem - 2px)` | +| `radiusLg` | The `rounded-lg` step | `0.5rem` | +| `radiusXl` | The `rounded-xl` step | `0.75rem` | +| `radius2xl` | The `rounded-2xl` step | `1rem` | +| `radius3xl` | The `rounded-3xl` step | `1.5rem` | +| `controlHeight` | Height of standard controls | `2.25rem` | +| `spaceCompact` | Compact spacing step | `0.375rem` | +| `spaceNormal` | Normal spacing step | `0.75rem` | +| `fontFamily` | UI font family (`font-sans`) | `Inter, sans-serif` | +| `monoFontFamily` | Code font family (`font-mono`) | `'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, 'Cascadia Mono', 'Liberation Mono', Consolas, monospace` | +| `elevationSurface` | Shadow of raised theme surfaces | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)` | +| `shadow2xs` | The `shadow-2xs` step | `0 1px rgb(0 0 0 / 0.05)` | +| `shadowXs` | The `shadow-xs` step | `0 1px 2px 0 rgb(0 0 0 / 0.05)` | +| `shadowSm` | The `shadow-sm` step and bare `shadow` | `0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)` | +| `shadowMd` | The `shadow-md` step | `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)` | +| `shadowLg` | The `shadow-lg` step | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)` | +| `shadowXl` | The `shadow-xl` step | `0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)` | +| `shadow2xl` | The `shadow-2xl` step | `0 25px 50px -12px rgb(0 0 0 / 0.25)` | +| `motionFast` | Duration of fast transitions | `150ms` | +| `motionNormal` | Duration of normal transitions | `200ms` | + +The defaults reproduce LibreChat's look, so a theme that sets none of these keys changes no shape. + +Accepted values: + +- **Radii, `controlHeight` and spacing:** `0`, or a number in `px`, `rem` or `em` (such as `0.25rem`), or a single `calc()` of two such lengths (such as `calc(0.5rem - 2px)`). +- **Font families:** any non-empty `font-family` list without `;`, `{` or `}`. The font must be available to the browser: LibreChat bundles only Inter and Roboto Mono, so any other family has to be installed on the viewer's machine or served by your deployment, or the next family in the list is used. +- **Shadow steps (`shadow2xs` through `shadow2xl`):** a concrete `box-shadow` list, or `none`. `var()`, `env()`, `attr()` and `url()` are rejected. +- **`elevationSurface`:** any non-empty `box-shadow` value without `;`, `{`, `}` or `url()`. +- **Motion:** a duration in `ms` or `s`, such as `120ms`. + +### Brands + +`brands` recolors the provider icons shown next to models. It can be set once at the top level for both modes, and overridden per mode under `modes..brands`. + +| Key | Default | +| --- | --- | +| `provider-openai` | `#19C37D` | +| `provider-openai-gpt4` | `#AB68FF` | +| `provider-openai-reasoning` | `#000000` | +| `provider-anthropic` | `#d09a74` | +| `provider-azure` | `linear-gradient(0.375turn, #61bde2, #4389d0)` | +| `provider-bedrock` | `#268672` | +| `provider-foreground` | `#ffffff` | + +Values are hex colors (`#rgb`, `#rrggbb` or `#rrggbbaa`). The fills may also be a `linear-gradient(...)`; `provider-foreground`, the icon glyph color, must be a hex color. From 4c3cced0cf509f51515f31cdbdbf609dca97e52c Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:10:44 +0200 Subject: [PATCH 2/4] docs: qualify theme rollback and list every setting the theme controls Removing interface.theme falls back to REACT_APP_THEME_* colors when the client was built with them, not to the stored theme, and the deployment theme also sets fonts, shadows and motion. --- .../configuration/librechat_yaml/object_structure/interface.mdx | 2 +- .../configuration/librechat_yaml/object_structure/theme.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/configuration/librechat_yaml/object_structure/interface.mdx b/content/docs/configuration/librechat_yaml/object_structure/interface.mdx index f97fd169c..c320e901b 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/interface.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/interface.mdx @@ -142,7 +142,7 @@ interface: Sets the deployment theme applied to every user, in both light and dark mode. Either the name of a bundled theme (`librechat` or `clickhouse`) or an inline theme definition with its own colors, radii, fonts, shadows and motion. -It outranks the `REACT_APP_THEME_*` build-time colors and users' stored themes, yields to the high-contrast modes, and is never saved to the user's browser, so removing it restores each user's own theme. +It outranks the `REACT_APP_THEME_*` build-time colors and users' stored themes, yields to the high-contrast modes, and is never saved to the user's browser. Removing it restores each user's own stored theme, or the `REACT_APP_THEME_*` colors when the client was built with them. **Key:** diff --git a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx index f7290dd26..df6b4a5b2 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx @@ -31,7 +31,7 @@ The client picks one theme, highest priority first: 3. **`REACT_APP_THEME_*`** build-time colors. See [Theme Colors](/docs/configuration/dotenv#theme-colors). 4. **The user's stored theme** in their browser. -The deployment theme only decides colors and shape. Each user still chooses light, dark or system mode themselves. +The deployment theme sets colors, shape, fonts, shadows and motion, but not the mode: each user still chooses light, dark or system themselves. The deployment theme is never written to the user's browser storage. If you remove `interface.theme`, users get their own stored theme back (or the `REACT_APP_THEME_*` colors, when the build sets them). From 348f491782317836ca104af90b2fa81f2f506d75 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:41:00 +0200 Subject: [PATCH 3/4] docs: bring the theme page to the current schema and document unknown-token tolerance --- .../librechat_yaml/object_structure/theme.mdx | 96 +++++++++++++++---- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx index df6b4a5b2..021a47e41 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx @@ -58,15 +58,9 @@ A name that is not one of these is ignored: the browser console logs `[Deploymen - **Accent.** Near-black `#151515` in light mode and the ClickHouse yellow `#faff69` in dark mode, used for the accent, submit button and focus ring. - **Radii.** A tighter scale taken from Click UI's `border.radii`: controls and `sm` through `lg` at `0.25rem`, surfaces and `xl`/`2xl` at `0.5rem`, large surfaces and `3xl` at `0.75rem`. - **Shadows.** Click UI's single elevation shadow on every raised surface, at 0.15 opacity in light mode and 0.6 in dark, with a hairline shadow for the `xs` and `sm` steps. -- **Fonts.** The UI font stays Inter. Code uses **Inconsolata**, followed by the same system monospace fallbacks as the default theme. - - - LibreChat only ships `@font-face` rules for Inter and Roboto Mono (see `client/src/style.css`). - Inconsolata renders only on machines that have it installed, or if your deployment serves it - itself, for example by adding an `@font-face` rule to a custom client build. Otherwise code - blocks fall back to `ui-monospace`, SF Mono, Menlo, Cascadia Mono, Liberation Mono or Consolas, - whichever is available. - +- **Controls.** Disabled controls use the `fill` style with their own disabled colors, the switch is a compact 2rem by 1rem, and table rows get a 1px rule. +- **Scrims.** Every dialog scrim is at 0.75 opacity. +- **Fonts.** The UI font stays Inter. Code uses **Inconsolata**, which LibreChat bundles, followed by the same system monospace fallbacks as the default theme. Headings and dialog titles ask for Basier Square first, which is not bundled, so they render in Inter unless the viewer has it installed. ## Inline theme definition @@ -114,24 +108,55 @@ interface: | `name` | String | Required. A non-empty name for the theme. | | `modes` | Object | Required. Holds `light` and/or `dark`. A mode you leave out uses LibreChat's defaults for that mode. | | `modes..colors` | Object | Color tokens for that mode. Keys are `rgb-*` token names, values are `R G B` triplets. See [Colors](#colors). | -| `modes..appearance` | Object | Shape, typography, shadow and motion values for that mode. See [Appearance](#appearance). | +| `modes..appearance` | Object | Shape, control, typography, scrim, shadow and motion values for that mode. See [Appearance](#appearance). | | `modes..brands` | Object | Provider brand colors for that mode. Overrides the theme-wide `brands`. | | `brands` | Object | Provider brand colors for both modes. See [Brands](#brands). | ### Validation -A definition is checked twice: +The server checks `interface.theme` with the same rules the browser applies before painting it, when `librechat.yaml` is loaded and on every config reload. + +**An invalid theme falls back to the default theme.** A wrong `version`, a missing `name`, a mode other than `light` or `dark`, an unknown field, an unknown brand token, or a value of the wrong kind for any token makes the theme unusable. LibreChat removes `interface.theme`, logs a warning with one line per problem, and loads the rest of the configuration as usual, so a theme mistake never stops the server: + +```text +Ignoring interface.theme in /app/librechat.yaml; the default theme applies instead: +- interface.theme.modes.dark.colors.rgb-surface-primary: Invalid RGB value for rgb-surface-primary: 300 16 32 +``` + +A bundled theme name that does not exist is handled the same way (`Unknown bundled theme "", expected one of: librechat, clickhouse`). + +**An unknown token is ignored, and the rest of the theme applies.** A color or appearance token that this version of LibreChat does not know, whether a typo or a token added in a newer version, costs only itself. The server logs it and leaves unknown colors out of the theme it sends to the browser: + +```text +interface.theme in /app/librechat.yaml names tokens this version ignores: +- interface.theme.modes.light.colors.rgb-surfce-secondary: Unknown light color token ignored: rgb-surfce-secondary +``` + +An unknown token is still checked for shape: a color name must be a plain lowercase token (letters, digits and `-`) holding an `R G B` value, and an appearance name must be camelCase holding a plain value without `;`, `{`, `}`, `<`, `>` or `url()`. Anything else is an error and the whole theme falls back. Check the server log after changing a theme, because a misspelled token is only reported there. -- **When `librechat.yaml` is loaded.** The shape must match: `version: 1`, a `name`, only `light` and `dark` under `modes`, only `colors`, `appearance` and `brands` inside a mode, color keys starting with `rgb-` and color values written as three space-separated numbers. A mismatch fails [configuration validation](/docs/configuration/dotenv#configuration-validation). -- **In the browser.** Token names and values are checked against the theme registry: unknown color, appearance or brand tokens, RGB channels above 255, and values of the wrong kind are rejected. If any check fails, the **whole** definition is ignored, the browser console logs `[DeploymentTheme] Ignoring invalid interface.theme: ...` with every error, and the app falls back to the next theme source. +The browser applies the same rules to the theme it receives and logs `[DeploymentTheme] Ignoring invalid interface.theme: ...` if it still has to reject one, for example when an older cached client meets a newer server. ### Colors -Colors use the same token names as the theme engine: `rgb-` followed by the token, such as `rgb-surface-primary`, `rgb-text-primary`, `rgb-border-medium`, `rgb-accent-primary` or `rgb-status-error-subtle`. The full list of 93 tokens is the `IThemeRGB` interface in [`packages/client/src/theme/types/index.ts`](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/client/src/theme/types/index.ts). +Colors use the same token names as the theme engine: `rgb-` followed by the token, such as `rgb-surface-primary`, `rgb-text-primary`, `rgb-border-medium`, `rgb-accent-primary` or `rgb-status-error-subtle`. The full list of 106 tokens is `themeColorTokens` in [`packages/data-provider/src/theme.ts`](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/data-provider/src/theme.ts), which the server and the browser both read, and each token is described in the `IThemeRGB` interface in [`packages/client/src/theme/types/index.ts`](https://github.com/LibreChat-AI/LibreChat/blob/canary/packages/client/src/theme/types/index.ts). + +Beyond the surface, text, border, status and syntax palettes, these roles let a theme restyle specific interaction states and components: + +| Role | Tokens | Paints | +| --- | --- | --- | +| Focus | `rgb-focus-outline`, `rgb-focus-control` | The keyboard focus outline across the app, and the focus ring of shared controls | +| Pressed | `rgb-surface-pressed`, `rgb-surface-inverted-pressed` | Neutral and inverted controls while pressed | +| Inverted and fixed | `rgb-surface-inverted`, `rgb-surface-inverted-hover`, `rgb-text-inverted`, `rgb-surface-fixed`, `rgb-surface-fixed-hover`, `rgb-text-fixed` | Controls drawn in the opposite mode's colors, and controls that keep one color in both modes | +| Disabled | `rgb-surface-disabled`, `rgb-text-disabled`, `rgb-border-disabled` | Disabled controls, when `disabledStyle` is `fill` (see [Appearance](#appearance)) | +| Control border | `rgb-border-control` | The edge of inputs, select triggers and one-time code slots, kept separate from quiet separators because it needs 3:1 contrast | +| Scrim | `rgb-surface-overlay` | The color behind dialogs; its strength is set by the scrim opacities in [Appearance](#appearance) | +| Switch | `rgb-switch-unchecked`, `rgb-switch-thumb` | The unchecked switch track, and the knob in both states | +| Table | `rgb-table-header-text`, `rgb-table-header-fill` | Column names, and the opaque fill of a sticky table header | +| Chart series | `rgb-series-1` through `rgb-series-8` | Categorical chart colors, in order | Each value is a bare `R G B` triplet with every channel from `0` to `255`, for example `'255 255 255'`. Hex values and `rgb(...)` are rejected. Quote the value so YAML reads it as a string. -A few tokens follow a related token you did set when you leave them out, so a partial palette stays coherent. For example, `rgb-text-muted` follows `rgb-text-tertiary`, and `rgb-surface-composer-hover` follows `rgb-surface-hover`. +A few tokens follow a related token you did set when you leave them out, so a partial palette stays coherent. For example, `rgb-text-muted` follows `rgb-text-tertiary`, `rgb-surface-composer-hover` and `rgb-surface-pressed` follow `rgb-surface-hover`, `rgb-focus-outline` follows `rgb-ring-primary`, `rgb-focus-control` follows `rgb-text-primary`, `rgb-border-control` follows `rgb-border-light`, `rgb-switch-thumb` follows `rgb-surface-primary`, `rgb-table-header-text` follows `rgb-text-secondary`, and `rgb-table-header-fill` follows `rgb-surface-dialog`. ### Appearance @@ -150,10 +175,31 @@ Appearance values are set **per mode**, and a mode without them uses the default | `radius2xl` | The `rounded-2xl` step | `1rem` | | `radius3xl` | The `rounded-3xl` step | `1.5rem` | | `controlHeight` | Height of standard controls | `2.25rem` | +| `switchWidth` | Width of the switch | `2.75rem` | +| `switchHeight` | Height of the switch; the knob is this minus the track's 4px border | `1.5rem` | +| `tableCellSpaceY` | Vertical padding of table cells | `1rem` | +| `tableRowStroke` | Thickness of the rule between table rows | `0px` | | `spaceCompact` | Compact spacing step | `0.375rem` | | `spaceNormal` | Normal spacing step | `0.75rem` | +| `disabledStyle` | How disabled controls look: `dim` fades them to 50% opacity, `fill` paints them with the disabled color roles | `dim` | | `fontFamily` | UI font family (`font-sans`) | `Inter, sans-serif` | | `monoFontFamily` | Code font family (`font-mono`) | `'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, 'Cascadia Mono', 'Liberation Mono', Consolas, monospace` | +| `displayFontFamily` | Headings and dialog titles (`font-display`); follows `fontFamily` when unset | `Inter, sans-serif` | +| `textXs` | The `text-xs` size | `0.75rem` | +| `textSm` | The `text-sm` size | `0.875rem` | +| `textBase` | The `text-base` size | `1rem` | +| `textLg` | The `text-lg` size | `1.125rem` | +| `textXl` | The `text-xl` size | `1.25rem` | +| `text2xl` | The `text-2xl` size | `1.5rem` | +| `leadingXs` | Line height paired with `text-xs` | `calc(1 / 0.75)` | +| `leadingSm` | Line height paired with `text-sm` | `calc(1.25 / 0.875)` | +| `leadingBase` | Line height paired with `text-base` | `calc(1.5 / 1)` | +| `leadingLg` | Line height paired with `text-lg` | `calc(1.75 / 1.125)` | +| `leadingXl` | Line height paired with `text-xl` | `calc(1.75 / 1.25)` | +| `leading2xl` | Line height paired with `text-2xl` | `calc(2 / 1.5)` | +| `scrimOpacity` | Strength of the scrim behind standard dialogs | `0.8` | +| `alertScrimOpacity` | Strength of the scrim behind confirmation dialogs | `0.9` | +| `modalScrimOpacity` | Strength of the scrim behind other modal dialogs | `0.65` | | `elevationSurface` | Shadow of raised theme surfaces | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)` | | `shadow2xs` | The `shadow-2xs` step | `0 1px rgb(0 0 0 / 0.05)` | | `shadowXs` | The `shadow-xs` step | `0 1px 2px 0 rgb(0 0 0 / 0.05)` | @@ -169,8 +215,13 @@ The defaults reproduce LibreChat's look, so a theme that sets none of these keys Accepted values: -- **Radii, `controlHeight` and spacing:** `0`, or a number in `px`, `rem` or `em` (such as `0.25rem`), or a single `calc()` of two such lengths (such as `calc(0.5rem - 2px)`). -- **Font families:** any non-empty `font-family` list without `;`, `{` or `}`. The font must be available to the browser: LibreChat bundles only Inter and Roboto Mono, so any other family has to be installed on the viewer's machine or served by your deployment, or the next family in the list is used. +- **Radii, `controlHeight`, spacing and text sizes:** `0`, or a number in `px`, `rem` or `em` (such as `0.25rem`), or a single `calc()` of two such lengths (such as `calc(0.5rem - 2px)`). +- **`switchWidth` and `switchHeight`:** a positive length in `px` or `rem`. Both must use the same unit (a side you leave out uses its `rem` default), the width must exceed the height so the knob can travel, and the height must clear the 4px track border (more than `4px`, or at least `0.5rem`). +- **`tableCellSpaceY` and `tableRowStroke`:** `0`, or a length in `px` or `rem`. +- **`disabledStyle`:** `dim` or `fill`. +- **Line heights:** a unitless number (such as `1.5`), a single `calc()` dividing two numbers (such as `calc(1.25 / 0.875)`), or a length. +- **Scrim opacities:** a number from `0` to `1`. +- **Font families:** any non-empty `font-family` list without `;`, `{` or `}`. The font must be available to the browser: LibreChat bundles Inter, Roboto Mono and Inconsolata, so any other family has to be installed on the viewer's machine or served by your deployment, or the next family in the list is used. - **Shadow steps (`shadow2xs` through `shadow2xl`):** a concrete `box-shadow` list, or `none`. `var()`, `env()`, `attr()` and `url()` are rejected. - **`elevationSurface`:** any non-empty `box-shadow` value without `;`, `{`, `}` or `url()`. - **Motion:** a duration in `ms` or `s`, such as `120ms`. @@ -190,3 +241,14 @@ Accepted values: | `provider-foreground` | `#ffffff` | Values are hex colors (`#rgb`, `#rrggbb` or `#rrggbbaa`). The fills may also be a `linear-gradient(...)`; `provider-foreground`, the icon glyph color, must be a hex color. + +## Using the theme outside LibreChat + +The `@librechat/client` package publishes the theme as a stylesheet, so another app built on its components can paint the same tokens. Import `@librechat/client/theme.css` in the stylesheet that imports Tailwind: + +```css filename="app.css" +@import 'tailwindcss'; +@import '@librechat/client/theme.css'; +``` + +It carries the semantic color tokens and their Tailwind mappings, the default value of every color and appearance token, and the `@font-face` rules for Inter, Roboto Mono and Inconsolata. The font files ship in the package as `@librechat/client/fonts/*`, so the app's bundler (Vite, webpack or esbuild) has to resolve and emit them; a setup that serves compiled CSS without a bundler has to serve those paths itself. A font is only downloaded when text renders in it. From 0dce7a9b8715d83169056567b70541ea220d421a Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:47:13 +0200 Subject: [PATCH 4/4] docs: say which source applies after an invalid theme and which keys are tolerated --- .../librechat_yaml/object_structure/theme.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx index 021a47e41..5d8164277 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/theme.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/theme.mdx @@ -48,7 +48,7 @@ A shared link paints the theme of the tenant that owns the link, not the theme o | `librechat` | The default LibreChat palette and shape. Setting it explicitly also overrides `REACT_APP_THEME_*` colors and users' stored themes. | | `clickhouse` | A reference theme built from ClickHouse's Click UI design tokens. See [ClickHouse theme](#clickhouse-theme). | -A name that is not one of these is ignored: the browser console logs `[DeploymentTheme] Ignoring unknown interface.theme ""` and the app falls back to the next source in the list above. +A name that is not one of these is ignored: the server logs it and drops `interface.theme` (see [Validation](#validation)), and the app falls back to the next source in the list above. ### ClickHouse theme @@ -116,7 +116,7 @@ interface: The server checks `interface.theme` with the same rules the browser applies before painting it, when `librechat.yaml` is loaded and on every config reload. -**An invalid theme falls back to the default theme.** A wrong `version`, a missing `name`, a mode other than `light` or `dark`, an unknown field, an unknown brand token, or a value of the wrong kind for any token makes the theme unusable. LibreChat removes `interface.theme`, logs a warning with one line per problem, and loads the rest of the configuration as usual, so a theme mistake never stops the server: +**An invalid theme is dropped as if `interface.theme` were unset.** A wrong `version`, a missing `name`, a mode other than `light` or `dark`, a field the definition format does not have (at the top level, such as `css`, or inside a mode, anything other than `colors`, `appearance` and `brands`), an unknown brand token, or a value of the wrong kind for any token makes the theme unusable. LibreChat removes `interface.theme`, logs a warning with one line per problem, and loads the rest of the configuration as usual, so a theme mistake never stops the server. Users then get the next source in [How the theme is chosen](#how-the-theme-is-chosen): the `REACT_APP_THEME_*` colors or their stored theme, or LibreChat's default theme when neither applies. The log calls this the default theme: ```text Ignoring interface.theme in /app/librechat.yaml; the default theme applies instead: @@ -125,7 +125,9 @@ Ignoring interface.theme in /app/librechat.yaml; the default theme applies inste A bundled theme name that does not exist is handled the same way (`Unknown bundled theme "", expected one of: librechat, clickhouse`). -**An unknown token is ignored, and the rest of the theme applies.** A color or appearance token that this version of LibreChat does not know, whether a typo or a token added in a newer version, costs only itself. The server logs it and leaves unknown colors out of the theme it sends to the browser: +The `colors` and `appearance` maps work differently from the fields around them. + +**An unknown token is ignored, and the rest of the theme applies.** A key inside `colors` or `appearance` that this version of LibreChat does not know, whether a typo or a token added in a newer version, costs only itself. The server logs it and leaves unknown colors out of the theme it sends to the browser: ```text interface.theme in /app/librechat.yaml names tokens this version ignores: