How Cherry Studio Implements Light and Dark Mode Theming in React
Cherry Studio uses a centralized React context called ThemeProvider that separates the user's saved preference (settedTheme) from the actively applied theme (actualTheme), enabling seamless support for light, dark, and system-default modes while integrating with Tailwind CSS and third-party libraries like Ant Design.
The theming system in the cherryhq/cherry-studio repository demonstrates a robust approach to managing UI appearance across desktop platforms. By leveraging React's context API and careful DOM manipulation, the application ensures consistent theming from custom components to syntax-highlighted code blocks.
Core Architecture of the Cherry Studio Theming System
ThemeMode Enum and Type Definitions
The foundation of the theming system rests on a strict type contract defined in src/renderer/src/types/index.ts. Here, the ThemeMode enum establishes three possible states:
// src/renderer/src/types/index.ts (around line 529)
export enum ThemeMode {
light = 'light',
dark = 'dark',
system = 'system'
}
This enum ensures type safety throughout the application, preventing invalid theme values and enabling exhaustive switch statements when handling theme-specific logic.
The ThemeProvider Context
The central nervous system of Cherry Studio's theming is ThemeProvider.tsx located at src/renderer/src/context/ThemeProvider.tsx. This component creates a React context that manages two distinct concepts:
settedTheme: The user's explicit choice (light, dark, or system), persisted across sessionsactualTheme: The currently computed theme (light or dark) actually applied to the UI
This separation allows the application to respect system preferences when settedTheme is set to system, while still maintaining a record of the user's preference.
How Light and Dark Mode Detection Works
Resolving System Preferences
When the provider mounts, it detects the operating system's color scheme preference using the standard Web API:
// src/renderer/src/context/ThemeProvider.tsx (lines 37-38)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const isDark = mediaQuery.matches
This detection runs immediately to prevent flash-of-unstyled-content (FOUC) and ensures the initial render matches the user's OS settings when in "system" mode.
Separating User Choice from Applied Theme
The provider maintains state for both concepts:
// Conceptual implementation based on ThemeProvider.tsx logic
const [settedTheme, setSettedTheme] = useState<ThemeMode>(ThemeMode.system);
const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light');
When settedTheme changes to system, the provider re-evaluates window.matchMedia to update actualTheme. When settedTheme is light or dark, actualTheme simply mirrors that choice.
DOM Integration and Tailwind Support
Body Attributes and CSS Classes
To enable both Tailwind's dark mode and custom CSS variables, ThemeProvider.tsx manipulates the document body directly:
// src/renderer/src/context/ThemeProvider.tsx (lines 53-60)
document.body.setAttribute('theme-mode', actualTheme)
document.body.setAttribute('os', os)
if (actualTheme === 'dark') {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
This dual approach ensures:
- Tailwind compatibility: The
darkclass enablesdark:prefixed utility classes - Custom styling: The
theme-modeattribute allows CSS selectors like[theme-mode="dark"]for styled-components or CSS modules - OS-specific styling: The
osattribute enables platform-specific tweaks (macOS, Windows, Linux)
Persisting Theme Preferences
User preferences survive application restarts through IPC communication with the main process:
// src/renderer/src/context/ThemeProvider.tsx (lines 85-89)
const setTheme = (theme: ThemeMode) => {
setSettedTheme(theme)
window.api.setTheme(theme) // Persists to electron-store or similar
}
The window.api.setTheme call ensures the preference is stored in the main process's storage, available immediately when the application restarts.
Integrating Third-Party Libraries
Ant Design Theme Adaptation
Cherry Studio uses Ant Design for UI components. The AntdProvider (located at src/renderer/src/context/AntdProvider.tsx) bridges the theme context with Ant Design's ConfigProvider:
// src/renderer/src/context/AntdProvider.tsx (lines 17-19)
<ConfigProvider
theme={{
algorithm: theme === ThemeMode.dark ? theme.darkAlgorithm : theme.defaultAlgorithm,
}}
>
This ensures Ant Design components automatically switch between light and dark algorithmic color palettes without manual CSS overrides.
Syntax Highlighting with Shiki
For code blocks, Cherry Studio uses Shiki for syntax highlighting. The CodeStyleProvider (at src/renderer/src/context/CodeStyleProvider.tsx) maps the current theme to appropriate Shiki themes:
// src/renderer/src/context/CodeStyleProvider.tsx (lines 72-90)
const getShikiTheme = (theme: ThemeMode) => {
return theme === ThemeMode.dark ? 'dark' : 'materialLight';
};
// Usage in provider
const shikiTheme = getShikiTheme(actualTheme);
This ensures code snippets remain readable in both modes, with high-contrast themes selected specifically for each mode.
Usage Examples
Accessing Theme in Components
Components consume the theme through the useTheme hook:
import { useTheme } from '@renderer/context/ThemeProvider';
import styled from 'styled-components';
const Card = styled.div<{ $isDark: boolean }>`
background: ${({ $isDark }) =>
$isDark ? 'var(--color-black)' : 'var(--color-white)'};
color: ${({ $isDark }) =>
$isDark ? 'var(--color-white)' : 'var(--color-black)'};
`;
export const ExampleCard = () => {
const { theme, toggleTheme } = useTheme();
const isDark = theme === 'dark';
return (
<Card $isDark={isDark}>
<p>Current mode: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</Card>
);
};
This pattern appears throughout the codebase, such as in Sidebar.tsx where components check settedTheme === ThemeMode.dark for conditional styling.
Building a Theme Switcher
A settings panel can offer all three options (light, dark, system):
import { useTheme, ThemeMode } from '@renderer/context/ThemeProvider';
import { Radio } from 'antd';
export const ThemeSettings = () => {
const { settedTheme, setTheme } = useTheme();
return (
<Radio.Group
value={settedTheme}
onChange={(e) => setTheme(e.target.value)}
>
<Radio value={ThemeMode.light}>Light</Radio>
<Radio value={ThemeMode.dark}>Dark</Radio>
<Radio value={ThemeMode.system}>System Default</Radio>
</Radio.Group>
);
};
When the user selects "System," the provider automatically syncs with OS changes via the matchMedia listener, while "Light" or "Dark" forces that specific mode regardless of OS settings.
Summary
- Centralized Context: Cherry Studio's theming system relies on a single
ThemeProvidercontext insrc/renderer/src/context/ThemeProvider.tsxthat manages both user preferences and active theme state. - Dual State Management: The system distinguishes between
settedTheme(user's saved choice: light/dark/system) andactualTheme(currently applied: light/dark), enabling proper system-preference fallback. - DOM Integration: The provider updates
<body>attributes (theme-mode,os) and toggles thedarkclass to activate Tailwind's dark mode variants and custom CSS selectors. - Persistence: Theme preferences survive restarts via IPC calls to
window.api.setTheme(), storing values in the main process. - Ecosystem Compatibility: Additional providers (
AntdProvider,CodeStyleProvider) adapt Ant Design and Shiki syntax highlighting to the current theme without duplicating logic.
Frequently Asked Questions
How does Cherry Studio detect the operating system's dark mode preference?
Cherry Studio uses the standard Web API window.matchMedia('(prefers-color-scheme: dark)') inside ThemeProvider.tsx to detect the OS color scheme on initial mount. When the user selects "System" mode, the provider listens to changes in this media query and updates the actualTheme state accordingly, ensuring the UI always matches the OS preference without requiring a page refresh.
What is the difference between settedTheme and actualTheme in Cherry Studio's codebase?
The settedTheme represents the user's explicit preference stored in settings—either light, dark, or system. The actualTheme is the computed value that is actually applied to the UI, which can only be light or dark. When settedTheme is system, actualTheme resolves to the current OS preference. This separation allows the application to remember that a user prefers "system" mode while still knowing whether to render light or dark UI elements at any given moment.
How does Cherry Studio integrate with Tailwind CSS for dark mode support?
The ThemeProvider in src/renderer/src/context/ThemeProvider.tsx manipulates the DOM directly by adding or removing the dark class on the <body> element based on the actualTheme value. Tailwind CSS is configured to use the "class" strategy for dark mode, which means any utility class prefixed with dark: only activates when an ancestor element (in this case, <body>) has the dark class. The provider also sets a theme-mode attribute for styled-components and CSS selectors that need theme-specific values.
Can third-party UI libraries automatically adapt to Cherry Studio's theme changes?
Yes, Cherry Studio includes specialized provider components that bridge the theme context to third-party libraries. The AntdProvider reads the current theme value and passes the appropriate algorithm—theme.darkAlgorithm or theme.defaultAlgorithm—to Ant Design's ConfigProvider, causing all Ant Design components to switch palettes automatically. Similarly, the CodeStyleProvider maps the theme to Shiki syntax highlighting themes (dark vs materialLight), ensuring code blocks maintain proper contrast in both modes without manual intervention from individual components.
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 →