packages/client/src/theme/README.md
This theme system allows you to dynamically change colors in your React application using CSS variables and Tailwind CSS. It combines dark/light mode switching with dynamic color theming capabilities.
The theme system provides:
The theme system operates in three layers:
style.css definitionshtml selector.dark selectorthemeRGB prop is providedrgb() formatted valuesnpm install @librechat/client
import { ThemeProvider } from '@librechat/client';
function App() {
return (
<ThemeProvider initialTheme="system">
<YourApp />
</ThemeProvider>
);
}
Ensure your app has CSS variables defined as fallbacks:
/* style.css */
:root {
--white: #fff;
--gray-800: #212121;
--gray-100: #ececec;
/* ... other color definitions */
}
html {
--text-primary: var(--gray-800);
--surface-primary: var(--white);
/* ... other theme variables */
}
.dark {
--text-primary: var(--gray-100);
--surface-primary: var(--gray-900);
/* ... other dark theme variables */
}
Update your tailwind.config.js:
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
// Include component library files
'./node_modules/@librechat/client/dist/**/*.js',
],
darkMode: ['class'],
theme: {
extend: {
colors: {
// Map CSS variables to Tailwind colors
'text-primary': 'var(--text-primary)',
'surface-primary': 'var(--surface-primary)',
'brand-purple': 'var(--brand-purple)',
// ... other colors
},
},
},
};
function MyComponent() {
return (
<div className="bg-surface-primary text-text-primary border border-border-light">
<h1 className="text-text-secondary">Hello World</h1>
<button className="bg-surface-submit hover:bg-surface-submit-hover text-white">
Submit
</button>
</div>
);
}
text-text-primary - Primary text colortext-text-secondary - Secondary text colortext-text-secondary-alt - Alternative secondary texttext-text-tertiary - Tertiary text colortext-text-warning - Warning text colorbg-surface-primary - Primary backgroundbg-surface-secondary - Secondary backgroundbg-surface-tertiary - Tertiary backgroundbg-surface-submit - Submit button backgroundbg-surface-destructive - Destructive action backgroundbg-surface-dialog - Dialog/modal backgroundbg-surface-chat - Chat interface backgroundborder-border-light - Light borderborder-border-medium - Medium borderborder-border-heavy - Heavy borderborder-border-xheavy - Extra heavy borderbg-brand-purple - Brand purple colorbg-presentation - Presentation backgroundring-ring-primary - Focus ring colorimport { IThemeRGB } from '@librechat/client';
export const customTheme: IThemeRGB = {
'rgb-text-primary': '0 0 0', // Black
'rgb-text-secondary': '100 100 100', // Gray
'rgb-surface-primary': '255 255 255', // White
'rgb-surface-submit': '0 128 0', // Green
'rgb-brand-purple': '138 43 226', // Blue Violet
// ... define other colors
};
import { ThemeProvider } from '@librechat/client';
import { customTheme } from './themes/custom';
function App() {
return (
<ThemeProvider themeRGB={customTheme} themeName="custom">
<YourApp />
</ThemeProvider>
);
}
Load theme colors from environment variables:
# .env.local
REACT_APP_THEME_BRAND_PURPLE=171 104 255
REACT_APP_THEME_TEXT_PRIMARY=33 33 33
REACT_APP_THEME_TEXT_SECONDARY=66 66 66
REACT_APP_THEME_SURFACE_PRIMARY=255 255 255
REACT_APP_THEME_SURFACE_SUBMIT=4 120 87
function getThemeFromEnv(): IThemeRGB | undefined {
// Check if any theme environment variables are set
const hasThemeEnvVars = Object.keys(process.env).some(key =>
key.startsWith('REACT_APP_THEME_')
);
if (!hasThemeEnvVars) {
return undefined; // Use default themes
}
return {
'rgb-text-primary': process.env.REACT_APP_THEME_TEXT_PRIMARY || '33 33 33',
'rgb-brand-purple': process.env.REACT_APP_THEME_BRAND_PURPLE || '171 104 255',
// ... other colors
};
}
<ThemeProvider
initialTheme="system"
themeRGB={getThemeFromEnv()}
>
<App />
</ThemeProvider>
The ThemeProvider handles dark/light mode automatically:
import { useTheme } from '@librechat/client';
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Current theme: {theme}
</button>
);
}
'light' - Force light mode'dark' - Force dark mode'system' - Follow system preferenceIf you're migrating from an older theme system:
Before:
import { ThemeContext, ThemeProvider } from '~/hooks/ThemeContext';
After:
import { ThemeContext, ThemeProvider } from '@librechat/client';
The new ThemeProvider is backward compatible but adds new capabilities:
<ThemeProvider
initialTheme="system" // Same as before
themeRGB={customTheme} // New: optional custom colors
>
<App />
</ThemeProvider>
Components using ThemeContext continue to work without changes:
// This still works!
const { theme, setTheme } = useContext(ThemeContext);
packages/client/src/theme/
├── context/
│ └── ThemeProvider.tsx # Main theme provider
├── types/
│ └── index.ts # TypeScript interfaces
├── themes/
│ ├── default.ts # Light theme colors
│ ├── dark.ts # Dark theme colors
│ └── index.ts # Theme exports
├── utils/
│ ├── applyTheme.ts # Apply CSS variables
│ ├── tailwindConfig.ts # Tailwind helpers
│ └── createTailwindColors.js
├── README.md # This documentation
└── index.ts # Main exports
The theme system uses RGB values in CSS variables:
--text-primary: rgb(33 33 33)'rgb-text-primary': '33 33 33'text-text-primaryAll color values must be in space-separated RGB format:
'255 255 255''#ffffff' or 'rgb(255, 255, 255)'This format allows Tailwind to apply opacity modifiers like bg-surface-primary/50.
themeRGB prop to ThemeProviderrgb(R G B) format--brand-purple: var(--brand-purple) creates infinite loop--brand-purple: #ab68ffdarkMode: ['class'] is in your Tailwind config<html> element should have class="dark" in dark modeIThemeRGB interface:import { IThemeRGB } from '@librechat/client';
import { ThemeProvider, defaultTheme, darkTheme } from '@librechat/client';
import { useState } from 'react';
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider
initialTheme={isDark ? 'dark' : 'light'}
themeRGB={isDark ? darkTheme : defaultTheme}
themeName={isDark ? 'dark' : 'default'}
>
<button onClick={() => setIsDark(!isDark)}>
Toggle Theme
</button>
<YourApp />
</ThemeProvider>
);
}
const themes = {
default: undefined, // Use CSS defaults
ocean: {
'rgb-brand-purple': '0 119 190',
'rgb-surface-primary': '240 248 255',
// ... ocean theme colors
},
forest: {
'rgb-brand-purple': '34 139 34',
'rgb-surface-primary': '245 255 250',
// ... forest theme colors
},
};
function App() {
const [selectedTheme, setSelectedTheme] = useState('default');
return (
<ThemeProvider
themeRGB={themes[selectedTheme]}
themeName={selectedTheme}
>
<select onChange={(e) => setSelectedTheme(e.target.value)}>
{Object.keys(themes).map(name => (
<option key={name} value={name}>{name}</option>
))}
</select>
<YourApp />
</ThemeProvider>
);
}
When using the ThemeProvider in your main application with localStorage persistence:
import { ThemeProvider } from '@librechat/client';
import { getThemeFromEnv } from './utils';
function App() {
const envTheme = getThemeFromEnv();
return (
<ThemeProvider
// Only pass props if you want to override stored values
// If you always pass props, they will override localStorage
initialTheme={envTheme ? "system" : undefined}
themeRGB={envTheme || undefined}
>
</ThemeProvider>
);
}
Important: Props passed to ThemeProvider will override stored values on initial mount. Only pass props when you explicitly want to override the user's saved preferences.
When adding new theme colors:
types/index.tsThis theme system is part of the @librechat/client package.