Skip to content

UI Theming

Transform your Pika application's appearance with a powerful theming system based on CSS custom properties and OKLCH colors. Your theme changes are protected from framework updates and take effect immediately with hot reload.

By the end of this guide, you will:

  • Enable custom theming in your project
  • Understand the theme variable system and color format
  • Customize your brand colors and semantic colors
  • Use CLI commands to manage theme updates
  • Apply themes to embedded web components
  • A running Pika installation
  • Basic understanding of CSS custom properties (variables)
  • Familiarity with color theory (helpful but not required)

A sample theme is provided as a starting point. Copy it and customize:

Terminal window
cd apps/pika-chat/src/lib/custom
cp sample-purple-theme.ts my-theme.ts

In pika-config.ts, enable custom theming and point to your theme file:

siteFeatures: {
uiCustomization: {
featureId: 'uiCustomization',
enabled: true,
customTheme: {
enabled: true,
// Path to your theme file (without .ts extension)
themeConfigPath: 'src/lib/custom/my-theme'
}
}
}

Edit your theme file (e.g., apps/pika-chat/src/lib/custom/my-theme.ts):

import type { ThemeConfig } from 'pika-shared/types/chatbot/theme-types';
export const themeConfig: ThemeConfig = {
schemaVersion: 1,
name: 'My Brand Theme',
cssVariables: {
light: {
// Your brand's primary color
primary: 'oklch(0.55 0.2 250)', // Blue
'primary-foreground': 'oklch(1 0 0)', // White text
},
dark: {
primary: 'oklch(0.65 0.18 250)',
}
}
};

The dev server automatically reloads when you save theme changes. No restart required.


Pika uses semantic CSS variables that describe purpose, not raw colors. This makes theming intuitive and maintainable.

The foundation of your brand identity.

VariableDescriptionWhat It Affects
primaryYour main brand colorPrimary buttons, links, active states, focus rings
primary-foregroundText/icons on primaryButton text, icons on primary backgrounds
secondarySecondary actionsSecondary buttons, badges, tags
secondary-foregroundText on secondarySecondary button text
destructiveDanger actionsDelete buttons, error states
destructive-foregroundText on destructiveDestructive button text

Example:

cssVariables: {
light: {
primary: 'oklch(0.47 0.2 290)', // Purple brand
'primary-foreground': 'oklch(1 0 0)', // White text
secondary: 'oklch(0.97 0.01 290)', // Light purple
'secondary-foreground': 'oklch(0.47 0.2 290)',
destructive: 'oklch(0.55 0.2 25)', // Red
}
}

You can customize or replace the AI sparkle icon that appears next to the chat app title.

Add the chat-app-icon CSS variable to change the default icon's color. Define values for both light and dark modes:

cssVariables: {
light: {
'chat-app-icon': 'oklch(0.55 0.16 195)', // Teal - visible on light backgrounds
},
dark: {
'chat-app-icon': 'oklch(0.70 0.14 195)', // Lighter teal - visible on dark backgrounds
}
}

To use your own logo or icon:

  1. Place your icon(s) in the assets folder:

    apps/pika-chat/static/custom/assets/

    This folder is protected from pika sync and files are served at /custom/assets/.

  2. Reference it in your theme config:

    Single icon (same for both modes):

    export const themeConfig: ThemeConfig = {
    chatAppHeaderIcon: '/custom/assets/my-logo.svg',
    };

    Separate icons for light/dark modes:

    export const themeConfig: ThemeConfig = {
    chatAppHeaderIcon: {
    light: '/custom/assets/logo-dark.svg', // Dark logo on light background
    dark: '/custom/assets/logo-light.svg' // Light logo on dark background
    },
    };

Fine-tune the icon dimensions with these CSS variables:

cssVariables: {
light: {
'chat-app-header-icon-height': '28px', // Height (width scales automatically)
'chat-app-header-icon-gap': '8px', // Space between icon and title
}
}
VariableDefaultDescription
chat-app-header-icon-height32pxHeight of the custom icon (width scales to maintain aspect ratio)
chat-app-header-icon-gap4pxSpace between the icon and the chat app title

Pika uses the OKLCH color format for themes. OKLCH provides perceptually uniform colors, making it easier to create harmonious palettes.

oklch(lightness chroma hue)
ComponentRangeDescription
Lightness0 to 10 = black, 1 = white
Chroma0 to ~0.40 = gray, higher = more saturated
Hue0 to 360Color wheel angle
HueColor
0-30Red/Orange
30-90Orange/Yellow
90-150Yellow/Green
150-210Green/Cyan
210-270Cyan/Blue
270-330Blue/Purple
330-360Purple/Red
// Brand colors
'oklch(0.55 0.2 250)' // Vibrant blue
'oklch(0.47 0.2 290)' // Rich purple
'oklch(0.55 0.16 142)' // Fresh green
'oklch(0.55 0.2 25)' // Alert red
'oklch(0.75 0.15 75)' // Warm amber
// Neutral variations
'oklch(0.97 0.005 260)' // Very light gray (almost white)
'oklch(0.88 0.01 260)' // Light gray (borders)
'oklch(0.45 0.03 260)' // Medium gray (secondary text)
'oklch(0.22 0.02 260)' // Dark gray (primary text)

For a cohesive theme, vary lightness and chroma while keeping the same hue:

customPalettes: {
brand: {
'50': 'oklch(0.97 0.01 290)', // Lightest
'100': 'oklch(0.94 0.02 290)',
'200': 'oklch(0.88 0.05 290)',
'300': 'oklch(0.78 0.10 290)',
'400': 'oklch(0.62 0.15 290)',
'500': 'oklch(0.47 0.20 290)', // Primary
'600': 'oklch(0.40 0.18 290)',
'700': 'oklch(0.33 0.15 290)',
'800': 'oklch(0.26 0.12 290)',
'900': 'oklch(0.20 0.08 290)', // Darkest
}
}

Here's what each variable affects in the UI:

Button TypeVariables Used
Primaryprimary background, primary-foreground text
Secondarysecondary background, secondary-foreground text
Destructivedestructive background, destructive-foreground text
GhostTransparent, foreground text, accent hover
Outlineborder border, foreground text, accent hover
┌─────────────────────────────────────┐ ← border
│ Card Title │ ← card-foreground
│ ───────────────────────────────── │ ← border (divider)
│ Card content goes here. │ ← card-foreground
│ Secondary info here. │ ← muted-foreground
│ │
│ [Primary Button] [Secondary] │
└─────────────────────────────────────┘
↑ card background
BadgeBackgroundText
Successsuccess-bgsuccess
Warningwarning-bgwarning
Infoinfo-bginfo
Errordestructive-bgdestructive
AIai-bgai
Label text ← foreground
┌─────────────────────────────┐ ← input (border)
│ Placeholder text │ ← muted-foreground
└─────────────────────────────┘
Help text below ← muted-foreground
[Focused state]
┌─────────────────────────────┐ ← ring (focus outline)
│ User input text │ ← foreground
└─────────────────────────────┘
┌──────────────────┐
│ Logo │ ← sidebar-foreground
├──────────────────┤ ← sidebar-border
│ ▸ Active Item │ ← sidebar-primary (bg), sidebar-primary-foreground
│ Nav Item │ ← sidebar-foreground
│ Nav Item │ ← sidebar-accent (hover bg)
│ ─────────── │ ← sidebar-border
│ Nav Item │
└──────────────────┘
↑ sidebar-background

Pika includes CLI commands to help manage your theme as the framework evolves.

Terminal window
pika theme check

Shows if your theme is up to date with the latest schema:

📦 Theme Schema Check
Current schema version: v1
Your theme version: v1
✓ Your theme is up to date!
Terminal window
pika theme update

Adds new variables as comments to your theme file when new variables are added to the framework.

Terminal window
pika theme list

Shows all available theme variables with descriptions and default values.

Terminal window
pika theme docs

Displays quick-start guide and usage information.


Your theme automatically supports light and dark modes. Define both:

cssVariables: {
light: {
background: 'oklch(1 0 0)', // White
foreground: 'oklch(0.22 0.02 260)', // Dark text
primary: 'oklch(0.47 0.2 290)',
card: 'oklch(1 0 0)',
},
dark: {
background: 'oklch(0.15 0.02 260)', // Dark
foreground: 'oklch(0.95 0.005 260)', // Light text
primary: 'oklch(0.70 0.18 290)', // Brighter for dark bg
card: 'oklch(0.18 0.02 260)',
}
}

If you're embedding Pika components as web components in other applications (like Angular or React apps), those components can inherit your theme.

Web components read CSS variables from the document root. Your host application should define these variables:

/* In your host application's global CSS */
:root {
--primary: oklch(0.47 0.2 290);
--primary-foreground: oklch(1 0 0);
--background: oklch(1 0 0);
/* ... other variables */
}
.dark {
--primary: oklch(0.70 0.18 290);
--background: oklch(0.15 0.02 260);
/* ... dark mode overrides */
}

Use the helper functions from pika-shared:

import { getThemeVariable, getPikaThemeTokens } from 'pika-shared/util/wc-utils';
// Get a single variable
const primaryColor = getThemeVariable('primary');
// Returns: "oklch(0.47 0.2 290)"
// Get all theme tokens
const tokens = getPikaThemeTokens();
// Returns: { primary: "oklch(...)", background: "oklch(...)", ... }

To map Pika variables to another design system (e.g., Angular Material):

// In your Angular application
:root {
// Set Pika variables that web components will use
--primary: #{$your-brand-color};
--primary-foreground: white;
// Also map to Material theme if needed
--mat-primary: var(--primary);
}

Here's a full theme configuration for a corporate brand:

import type { ThemeConfig } from 'pika-shared/types/chatbot/theme-types';
export const themeConfig: ThemeConfig = {
schemaVersion: 1,
name: 'Acme Corp Theme',
fontFamily: '"Inter", "Segoe UI", Arial, sans-serif',
cssVariables: {
light: {
// Brand Colors (Teal)
primary: 'oklch(0.55 0.14 195)',
'primary-foreground': 'oklch(1 0 0)',
secondary: 'oklch(0.97 0.01 195)',
'secondary-foreground': 'oklch(0.40 0.10 195)',
// Surfaces
background: 'oklch(0.995 0.002 260)',
foreground: 'oklch(0.20 0.02 260)',
card: 'oklch(1 0 0)',
'card-foreground': 'oklch(0.20 0.02 260)',
popover: 'oklch(1 0 0)',
'popover-foreground': 'oklch(0.20 0.02 260)',
muted: 'oklch(0.97 0.003 260)',
'muted-foreground': 'oklch(0.50 0.02 260)',
accent: 'oklch(0.96 0.01 195)',
'accent-foreground': 'oklch(0.40 0.10 195)',
// Borders
border: 'oklch(0.90 0.008 260)',
input: 'oklch(0.90 0.008 260)',
ring: 'oklch(0.55 0.14 195)',
// Destructive
destructive: 'oklch(0.55 0.2 25)',
'destructive-foreground': 'oklch(1 0 0)',
// Status (harmonized with brand)
success: 'oklch(0.55 0.15 155)',
'success-foreground': 'oklch(1 0 0)',
'success-bg': 'oklch(0.95 0.04 155)',
warning: 'oklch(0.72 0.14 65)',
'warning-foreground': 'oklch(0.20 0.02 65)',
'warning-bg': 'oklch(0.96 0.04 65)',
info: 'oklch(0.55 0.14 240)',
'info-foreground': 'oklch(1 0 0)',
'info-bg': 'oklch(0.94 0.04 240)',
ai: 'oklch(0.55 0.14 195)',
'ai-foreground': 'oklch(1 0 0)',
'ai-bg': 'oklch(0.94 0.04 195)',
'destructive-bg': 'oklch(0.95 0.06 25)',
// Sidebar (subtle brand tint)
'sidebar-background': 'oklch(0.98 0.004 195)',
'sidebar-foreground': 'oklch(0.35 0.02 260)',
'sidebar-primary': 'oklch(0.55 0.14 195)',
'sidebar-primary-foreground': 'oklch(1 0 0)',
'sidebar-accent': 'oklch(0.95 0.01 195)',
'sidebar-accent-foreground': 'oklch(0.30 0.02 260)',
'sidebar-border': 'oklch(0.90 0.01 195)',
'sidebar-ring': 'oklch(0.55 0.14 195)',
// Charts (brand-coordinated palette)
'chart-1': 'oklch(0.55 0.14 195)',
'chart-2': 'oklch(0.55 0.15 155)',
'chart-3': 'oklch(0.72 0.14 65)',
'chart-4': 'oklch(0.55 0.14 240)',
'chart-5': 'oklch(0.55 0.15 320)',
},
dark: {
primary: 'oklch(0.70 0.12 195)',
'primary-foreground': 'oklch(0.15 0.02 195)',
secondary: 'oklch(0.25 0.03 195)',
'secondary-foreground': 'oklch(0.85 0.06 195)',
background: 'oklch(0.13 0.015 260)',
foreground: 'oklch(0.95 0.005 260)',
card: 'oklch(0.16 0.015 260)',
'card-foreground': 'oklch(0.95 0.005 260)',
popover: 'oklch(0.16 0.015 260)',
'popover-foreground': 'oklch(0.95 0.005 260)',
muted: 'oklch(0.22 0.015 260)',
'muted-foreground': 'oklch(0.68 0.02 260)',
accent: 'oklch(0.25 0.03 195)',
'accent-foreground': 'oklch(0.85 0.06 195)',
border: 'oklch(0.28 0.015 260)',
input: 'oklch(0.28 0.015 260)',
ring: 'oklch(0.65 0.10 195)',
destructive: 'oklch(0.50 0.18 25)',
'destructive-foreground': 'oklch(0.95 0.01 25)',
success: 'oklch(0.62 0.14 155)',
'success-bg': 'oklch(0.22 0.06 155)',
warning: 'oklch(0.68 0.12 65)',
'warning-bg': 'oklch(0.24 0.06 65)',
info: 'oklch(0.60 0.12 240)',
'info-bg': 'oklch(0.22 0.06 240)',
ai: 'oklch(0.60 0.12 195)',
'ai-bg': 'oklch(0.22 0.06 195)',
'destructive-bg': 'oklch(0.24 0.08 25)',
'sidebar-background': 'oklch(0.15 0.01 195)',
'sidebar-foreground': 'oklch(0.85 0.02 260)',
'sidebar-primary': 'oklch(0.65 0.12 195)',
'sidebar-primary-foreground': 'oklch(0.13 0.01 195)',
'sidebar-accent': 'oklch(0.22 0.02 195)',
'sidebar-accent-foreground': 'oklch(0.85 0.02 260)',
'sidebar-border': 'oklch(0.28 0.02 195)',
'sidebar-ring': 'oklch(0.60 0.10 195)',
'chart-1': 'oklch(0.65 0.12 195)',
'chart-2': 'oklch(0.62 0.14 155)',
'chart-3': 'oklch(0.68 0.12 65)',
'chart-4': 'oklch(0.60 0.12 240)',
'chart-5': 'oklch(0.60 0.12 320)',
}
},
customPalettes: {
brand: {
'50': 'oklch(0.97 0.01 195)',
'100': 'oklch(0.94 0.02 195)',
'200': 'oklch(0.88 0.04 195)',
'300': 'oklch(0.78 0.08 195)',
'400': 'oklch(0.65 0.11 195)',
'500': 'oklch(0.55 0.14 195)',
'600': 'oklch(0.47 0.12 195)',
'700': 'oklch(0.38 0.10 195)',
'800': 'oklch(0.30 0.08 195)',
'900': 'oklch(0.22 0.06 195)',
}
}
};

Begin by setting your brand's primary color. Derive other colors from it for visual harmony.

Always preview your theme in both light and dark modes. Colors that look great in light mode may need adjustment for dark mode.

Ensure text remains readable:

  • foreground on background: at least 4.5:1 contrast
  • primary-foreground on primary: at least 4.5:1 contrast

Override semantic variables rather than individual component styles. This ensures consistency across all components.

When you run pika sync and see a theme update notification, run pika theme check to see new variables you might want to customize.