Skip to content
Pilotoo
v0.1

Theming

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.

The demo web app’s stylesheet is the whole recipe for @pilotoo/ui.

apps/web/src/global.css
@import "tailwindcss";
@import "@pilotoo/ui/tokens.css";
@import "@pilotoo/ui/fonts.css";
@source "../node_modules/@pilotoo/ui/dist";
  • tokens.css defines the raw variables (--primary, --fill-bg-modal, --surface-sidebar, …) and surfaces them to Tailwind through @theme.
  • fonts.css is opt-in. It bundles Source Sans 3 and JetBrains Mono, offline.
  • @source points Tailwind at the installed package. Automatic detection never descends into node_modules, so without this line no utility used inside @pilotoo/ui is 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.

apps/web/src/main.tsx
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";

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 }.

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.

app.css
@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.

app.css
:root {
--primary: var(--brand-500);
--primary-hover: var(--brand-600);
--surface-sidebar: var(--app-sidebar);
}

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.

apps/web/src/main.tsx
theme: {
apply: (preference) => {
applyTheme(preference);
void syncNativeTheme(preference);
},
},