Theming
Introduction
Section titled “Introduction”The chat components are styled with Tailwind v4 and a set of CSS variables, the design tokens. Your app has to do two things before anything looks right: import the tokens and tell Tailwind where the component classes live. Everything after that, such as the accent colour or light and dark mode, is overriding variables.
Setting up the stylesheet
Section titled “Setting up the stylesheet”The demo web app’s stylesheet is the whole recipe for @pilotoo/ui.
@import "tailwindcss";@import "@pilotoo/ui/tokens.css";@import "@pilotoo/ui/fonts.css";
@source "../node_modules/@pilotoo/ui/dist";tokens.cssdefines the raw variables (--primary,--fill-bg-modal,--surface-sidebar, …) and surfaces them to Tailwind through@theme.fonts.cssis opt-in. It bundles Source Sans 3 and JetBrains Mono, offline.@sourcepoints Tailwind at the installed package. Automatic detection never descends intonode_modules, so without this line no utility used inside@pilotoo/uiis generated. The path is relative to the CSS file.
Import the stylesheet from your entry module rather than linking it from index.html, so it goes through the Tailwind pipeline.
import './global.css';For React Native (@pilotoo/chat-ui) the same idea applies with a different entry:
@import "tailwindcss";@import "uniwind";@import "@pilotoo/chat-ui/theme.css";
@source "../node_modules/@pilotoo/chat-ui/dist";Following light, dark or the system
Section titled “Following light, dark or the system”data-theme="light|dark" on <html> forces a scheme. With no attribute the system preference decides. applyTheme sets it, and themeControl stores the reader’s choice.
import { themeControl, useThemePreference, applyTheme, ThemePicker } from '@pilotoo/ui/settings';import { themePreferenceStore } from '@pilotoo/chat-core';
const control = themeControl(themePreferenceStore(storage), { apply: (p) => applyTheme(p) });
const [preference, choose] = useThemePreference(control);<ThemePicker value={preference} onChange={choose} />;themeControl reads the stored choice when it is created, so the first frame is already right. A preset option is your decision as the host, and the stored preference does not overrule it. storage is any KeyValueStorage; the demo web app passes browserStorage(localStorage) and hands control to the environment as theme: { apply }.
Matching your brand
Section titled “Matching your brand”Override the variables after the import. To change the accent, override --primary*, --sidebar-selected and --selection in the light block, in :root[data-theme='dark'] and in the dark media query.
@import "@pilotoo/ui/tokens.css";
:root { --primary: #0b6bcb; --primary-hover: #0954a0;}:root[data-theme='dark'] { --primary: #4da3ff; --primary-hover: #7bbcff;}@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { --primary: #4da3ff; --primary-hover: #7bbcff; }}If your app already has its own design tokens, map the kit’s variables onto them instead of copying values. That keeps the chat in step when your theme changes.
:root { --primary: var(--brand-500); --primary-hover: var(--brand-600); --surface-sidebar: var(--app-sidebar);}Desktop windows
Section titled “Desktop windows”In a Tauri app, call applyPlatformClasses() (or useVibrancy()) from @pilotoo/ui/desktop once at startup. On macOS it adds vibrancy to <html>, which turns the sidebar surface into a translucent tint (--surface-sidebar-vibrancy, overridable).
Call syncNativeTheme(preference) next to applyTheme so the window material follows the app theme. It needs the core:window:allow-set-theme capability and does nothing outside Tauri.
theme: { apply: (preference) => { applyTheme(preference); void syncNativeTheme(preference); },},Next steps
Section titled “Next steps”- Embedding the web chat scopes the same styles to one element.
- Chat UI lists the components that read these tokens.
- Voice and Presentation add optional UI that uses the same tokens.