How Dify Chat Ensures Visual Consistency with Its Theme Package: CSS Variables and Design Tokens
Dify Chat centralizes all visual design tokens in the @dify-chat/theme package and distributes them through CSS custom properties, a React context provider, and a shared color configuration to guarantee pixel-perfect consistency across light, dark, and system modes.
The lexmin0412/dify-chat repository solves the challenge of maintaining a unified visual language across multiple UI frameworks by implementing a centralized theming system. By storing every color value as a CSS variable and exposing them through a dedicated theme package, Dify Chat ensures visual consistency with its theme package regardless of whether users select light, dark, or system preferences.
CSS Custom Properties as the Source of Truth
All visual colors are stored as CSS variables in packages/react-app/src/App.css. The :root selector defines light mode values, while the .dark class overrides them for dark mode:
/* packages/react-app/src/App.css */
:root {
--theme-text-color: #333;
--theme-bg-color: #f2f4f7;
/* …other vars… */
}
/* dark mode overrides */
.dark {
--theme-text-color: #c9c9c9;
--theme-bg-color: #000;
/* …other vars… */
}
These variables are referenced using Tailwind’s arbitrary value syntax or custom class shortcuts. Because the variables live in one file, any color tweak instantly propagates to every UI element.
Runtime Switching via ThemeContextProvider
The ThemeContextProvider in packages/theme/src/hooks/index.tsx manages the current theme state and mode selection. It listens to system preferences and updates the DOM accordingly:
// packages/theme/src/hooks/index.tsx
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
if (themeMode === ThemeModeEnum.SYSTEM) {
handleColorSchemeChange(mediaQuery) // initialise
mediaQuery.addEventListener('change', handleColorSchemeChange)
} else {
// manual mode – force class & CSS vars
themeMode === ThemeModeEnum.DARK
? (document.body.classList.add(DARK_CLASS_NAME), setThemeState(ThemeEnum.DARK))
: (document.body.classList.remove(DARK_CLASS_NAME), setThemeState(ThemeEnum.LIGHT))
}
}, [themeMode])
When the mode changes, the provider toggles the dark class on <body> and updates CSS variables, causing all consuming components to re-render with the correct palette.
Bridging Tokens to Tailwind and Ant Design
The colors object in packages/react-app/src/theme/config.ts maps CSS variable names to Tailwind and Ant Design tokens:
// packages/react-app/src/theme/config.ts
export const colors = {
'theme-text': 'var(--theme-text-color)',
'theme-bg': 'var(--theme-bg-color)',
// …other tokens…
}
Tailwind integration: The colors map is merged into tailwind.config.js, enabling utility classes like bg-theme-bg and text-theme-text.
Ant Design integration: The same map populates the theme token object in the application root:
<XProvider theme={{ token: { colorPrimary: colors.primary, colorText: colors['theme-text'] } }} />
This dual integration ensures both styling systems read from the exact same source, preventing visual drift between framework components and custom elements.
Implementing Consistent UI Components
All UI markup uses token-based class names that resolve to the current CSS variable value. For example, the authentication page uses:
<div className="flex h-screen w-screen flex-col items-center justify-center bg-theme-bg">
<div className="text-theme-text">授权登录中…</div>
</div>
Because these class names remain constant while the underlying CSS variables change, components maintain visual consistency across theme switches without conditional rendering logic.
Consuming the Theme Package
To use the theme system, wrap your application with the provider in packages/react-app/src/index.tsx:
import { ThemeContextProvider } from '@dify-chat/theme'
const Root = () => (
<ThemeContextProvider>
<App />
</ThemeContextProvider>
)
Access the theme state in any component using the useThemeContext hook:
import { useThemeContext } from '@dify-chat/theme'
export const SampleBox = () => {
const { isDark, themeMode, setThemeMode } = useThemeContext()
return (
<div className="p-4 bg-theme-bg text-theme-text">
当前模式:{themeMode} ({isDark ? '暗色' : '浅色'})
<button onClick={() => setThemeMode('light')}>浅色</button>
<button onClick={() => setThemeMode('dark')}>暗色</button>
</div>
)
}
To embed a theme selector in your layout, use the ThemeSelector component from packages/react-app/src/components/layout/header.tsx:
import { ThemeSelector } from '@dify-chat/theme'
export const Header = () => (
<header>
{/* other header content */}
<ThemeSelector>
<button className="text-theme-text">切换主题</button>
</ThemeSelector>
</header>
)
Summary
- CSS Variables: All color values are defined in
packages/react-app/src/App.cssand scoped to:rootand.darkselectors, creating a single source of truth for the entire application. - Context Provider: The
ThemeContextProviderhandles runtime switching between light, dark, and system modes by manipulating thedarkclass on<body>and listening toprefers-color-schememedia queries. - Design Tokens: The
colorsconfiguration inpackages/react-app/src/theme/config.tsbridges CSS variables to both Tailwind CSS and Ant Design, ensuring framework-agnostic visual consistency. - Component Integration: UI elements use token-based class names (e.g.,
bg-theme-bg) that automatically resolve to the correct color values regardless of the active theme.
Frequently Asked Questions
How does Dify Chat handle system-level dark mode preferences?
The ThemeContextProvider initializes a window.matchMedia('(prefers-color-scheme: dark)') listener when the theme mode is set to SYSTEM. When the media query changes, the provider automatically toggles the dark class on the document body and updates the theme state, ensuring the UI reflects OS-level preferences without requiring a page refresh.
Can I use Dify Chat theme tokens in custom components?
Yes. Any component within the ThemeContextProvider tree can import useThemeContext from @dify-chat/theme to access the current theme state. For styling, use the predefined token classes like bg-theme-bg or text-theme-text, which resolve to the active CSS variables defined in packages/react-app/src/App.css.
Where are the theme color values actually defined?
The raw color values are defined as CSS custom properties in packages/react-app/src/App.css within the :root and .dark selectors. These variables are then mapped to JavaScript-friendly tokens in packages/react-app/src/theme/config.ts for consumption by Tailwind and Ant Design configurations.
What happens when a user manually switches themes?
When setThemeMode is called with 'light' or 'dark', the provider immediately updates the local storage persistence, toggles the dark class on <body>, and sets the internal theme state. This triggers a re-render of all components using useThemeContext, while CSS variables update automatically, ensuring instant visual feedback without flashing or layout shifts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →