Documentschainlit
Chainlit Theme System
Chainlit Theme System
Type
Topic
Status
Published
Created
Jul 7, 2026
Updated
Jul 7, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Chainlit Theme System#

Overview#

Chainlit has two parallel ThemeProvider implementations: one for the main frontend app and one for the copilot widget. Both are custom React Context providers — they do not use shadcn/ui's built-in theming directly. The key distinction is their DOM target: the frontend provider writes to document.documentElement, while the copilot provider writes to a Shadow DOM element.


Frontend ThemeProvider#

Source: frontend/src/components/ThemeProvider.tsx

  • Creates a ThemeProviderContext with state for 'light' | 'dark' | 'system' themes .
  • On theme change, removes existing light/dark classes from document.documentElement and adds the resolved class .
  • Applies custom CSS variables from window.theme to the document root via root.style.setProperty() .
  • Persists user selection to localStorage under the key vite-ui-theme .
  • Exports useTheme(), which resolves 'system' to the actual OS preference using window.matchMedia('(prefers-color-scheme: dark)') and returns a variant field ('light' or 'dark') .

Copilot ThemeProvider (Shadow DOM)#

Source: libs/copilot/src/ThemeProvider.tsx

The copilot widget is mounted inside a Shadow DOM for style isolation. The entry point (libs/copilot/index.tsx) calls container.attachShadow({ mode: 'open' }), creates a <div id="cl-shadow-root"> inside it, and stores a reference at window.cl_shadowRootElement .

The copilot ThemeProvider is structurally identical to the frontend version but targets window.cl_shadowRootElement instead of document.documentElement :

  • Theme classes (light/dark) are toggled on window.cl_shadowRootElement .
  • CSS variables from window.theme are applied to the shadow container's inline style .
  • A matchMedia listener responds to OS theme changes when theme === 'system' .

Theme initialization flow:

mountChainlitWidget() (libs/copilot/index.tsx)
  └─ AppWrapper (libs/copilot/src/appWrapper.tsx)
       ├─ fetches /public/theme.json → sets window.theme (line 38-53)
       └─ App
            └─ ThemeProvider defaultTheme from widgetConfig.theme or server (app.tsx:56, 90)

CSS styles (Tailwind, Sonner, highlight.js) are injected as inline <style> tags inside the shadow root to keep the widget fully isolated .


Known Bug: Logo Reads Wrong ThemeProvider Context#

Source: frontend/src/components/Logo.tsx

The Logo component imports useTheme from ./ThemeProvider — the frontend ThemeProvider . When Logo is rendered inside the copilot widget, the React component tree crosses the Shadow DOM boundary but the module import still resolves to the frontend context. If no frontend ThemeProvider ancestor is present, the context falls back to the module-level initialState default (theme: 'system') , which causes useTheme() to resolve the theme from window.matchMedia('(prefers-color-scheme: dark)') — i.e., the OS color scheme — rather than the widget-configured theme .

The practical symptom: the logo shown inside the copilot widget follows the host OS theme, not the widgetConfig.theme setting.

Fix direction: Logo should import useTheme from the copilot's ThemeProvider when rendered in the copilot context, or both providers should be unified via a shared context in a common package. PR #2960 also touches Logo.tsx for unrelated URL-resolution fixes, indicating active churn in this component.


Known Bug: Transparent UI in Copilot Light Mode#

PR: #2768 – Fix: Resolve transparent UI issue in Copilot Light Mode

The CSS variables for light mode in libs/copilot/src/index.css were scoped to #shadow-root-container, but the actual shadow root element ID is #cl-shadow-root. This meant light mode base variables were never applied, causing transparency. Dark mode was unaffected because it used class-based overrides. The fix:

  • Retargeted the base variables to #cl-shadow-root (light mode default).
  • Scoped dark mode to #cl-shadow-root.dark.
  • Added an explicit #cl-shadow-root.light selector.


Key Files at a Glance#

FilePurpose
frontend/src/components/ThemeProvider.tsxFrontend theme context; targets document.documentElement
libs/copilot/src/ThemeProvider.tsxCopilot theme context; targets window.cl_shadowRootElement
libs/copilot/index.tsxMounts Shadow DOM, sets window.cl_shadowRootElement
libs/copilot/src/appWrapper.tsxFetches /public/theme.json, populates window.theme
libs/copilot/src/app.tsxMounts copilot ThemeProvider with defaultTheme from widget config
frontend/src/components/Logo.tsxConsumes frontend useTheme — source of the cross-context bug