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
ThemeProviderContextwith state for'light' | 'dark' | 'system'themes . - On theme change, removes existing
light/darkclasses fromdocument.documentElementand adds the resolved class . - Applies custom CSS variables from
window.themeto the document root viaroot.style.setProperty(). - Persists user selection to
localStorageunder the keyvite-ui-theme. - Exports
useTheme(), which resolves'system'to the actual OS preference usingwindow.matchMedia('(prefers-color-scheme: dark)')and returns avariantfield ('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 onwindow.cl_shadowRootElement. - CSS variables from
window.themeare applied to the shadow container's inline style . - A
matchMedialistener responds to OS theme changes whentheme === '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.lightselector.
Key Files at a Glance#
| File | Purpose |
|---|---|
frontend/src/components/ThemeProvider.tsx | Frontend theme context; targets document.documentElement |
libs/copilot/src/ThemeProvider.tsx | Copilot theme context; targets window.cl_shadowRootElement |
libs/copilot/index.tsx | Mounts Shadow DOM, sets window.cl_shadowRootElement |
libs/copilot/src/appWrapper.tsx | Fetches /public/theme.json, populates window.theme |
libs/copilot/src/app.tsx | Mounts copilot ThemeProvider with defaultTheme from widget config |
frontend/src/components/Logo.tsx | Consumes frontend useTheme — source of the cross-context bug |