@personaai/ui components is applied via CSS custom properties with literal zinc fallbacks — an unthemed app gets the default zinc palette, and a themed app gets its brand colors. The mechanism is slightly unusual but deliberately designed: every theme value is a plain CSS variable with a fallback, so passing your own var(--your-token) works and picks up your existing light/dark switching for free.
How it works
When you pass atheme prop, the component computes a CSSProperties object where each theme field maps to a --persona-* CSS variable. This is applied as an inline style on a root element that wraps the entire widget.
Every component reads its colors through var(--persona-x, <default>) in its Tailwind classes:
-
Both light and dark classes use the same
--persona-*variable with the same fallback. A theme value is applied uniformly across light and dark mode — the same color both times. This is intentional: a brand’s primary color doesn’t usually change between modes, and if it does, you can use your ownvar(--brand-primary)which resolves differently in each mode. - The fallback is the original hardcoded value — so an unthemed app produces pixel-identical output to a widget without the theme prop at all.
PersonaCustomTheme — full shape
undefined style properties, so the CSS class fallback takes effect.
CSS variable map
ThebuildThemeStyles utility produces:
Using your own design tokens
The key insight: since every value is a CSS custom property, passingvar(--your-token) as a theme value creates a chain of CSS variable indirection:
bg-[var(--persona-bg,#ffffff)], CSS resolves:
var(--persona-bg)→var(--surface-bg)(from inline style)var(--surface-bg)→ your actual color value (from your stylesheet)
- Your existing light/dark switching works automatically — if
--surface-bgresolves to#ffffffin light mode and#1a1a1ain dark mode, the widget follows. - No extra JS required — it’s pure CSS variable resolution.
- The zinc fallback is never reached — because
--persona-bgis set, even though its value is anothervar().
Example: Tailwind CSS + custom theme
Example: Chakra UI / CSS-in-JS
Theme propagation in PersonaChatLauncher
PersonaChatLauncher has a subtlety: the FAB button is a sibling of PersonaChatView’s subtree, not a descendant. CSS custom properties set on PersonaChatView’s root don’t reach the FAB.
The launcher solves this by computing buildThemeStyles once and applying it to a common ancestor of both the FAB and the panel. PersonaChatView also sets the same styles on its own root (harmless duplication), but the launcher’s wrapper ensures the FAB picks up primaryColor for its background.
This is handled automatically — no configuration needed. If you’re building your own layout with a FAB-style button and PersonaMessageFeed, apply buildThemeStyles to a parent element that wraps both.
What’s NOT controlled by theme
- Typography — font family, size, and weight are hardcoded (system font stack, consistent sizing). Theme controls color only.
- Spacing — padding and gap values are fixed.
- Layout — sidebar width, drawer width, panel dimensions are fixed (or controlled by props like
panelWidth). - Shadows and borders — border widths and shadow levels are not themeable (only border color is).