Skip to main content
Every color in @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 a theme 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:
Two things to notice:
  1. 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 own var(--brand-primary) which resolves differently in each mode.
  2. 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

All fields are optional. Unset fields are omitted from the inline style entirely — React doesn’t render undefined style properties, so the CSS class fallback takes effect.

CSS variable map

The buildThemeStyles utility produces:

Using your own design tokens

The key insight: since every value is a CSS custom property, passing var(--your-token) as a theme value creates a chain of CSS variable indirection:
The rendered inline style becomes:
When a component renders bg-[var(--persona-bg,#ffffff)], CSS resolves:
  1. var(--persona-bg) → var(--surface-bg) (from inline style)
  2. var(--surface-bg) → your actual color value (from your stylesheet)
This means:
  • Your existing light/dark switching works automatically — if --surface-bg resolves to #ffffff in light mode and #1a1a1a in dark mode, the widget follows.
  • No extra JS required — it’s pure CSS variable resolution.
  • The zinc fallback is never reached — because --persona-bg is set, even though its value is another var().

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