# How Cherry Studio Implements Light and Dark Mode Theming in React

> Discover how Cherry Studio implements light and dark mode theming in React using a centralized context and Tailwind CSS. Learn to seamlessly switch themes, including system defaults.

- Repository: [CherryHQ/cherry-studio](https://github.com/cherryhq/cherry-studio)
- Tags: internals
- Published: 2026-02-27

---

**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`](https://github.com/cherryhq/cherry-studio/blob/main/src/renderer/src/types/index.ts). Here, the `ThemeMode` enum establishes three possible states:

```typescript
// 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`](https://github.com/cherryhq/cherry-studio/blob/main/ThemeProvider.tsx) located at [`src/renderer/src/context/ThemeProvider.tsx`](https://github.com/cherryhq/cherry-studio/blob/main/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 sessions
- **`actualTheme`**: 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:

```typescript
// 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:

```typescript
// 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`](https://github.com/cherryhq/cherry-studio/blob/main/ThemeProvider.tsx) manipulates the document body directly:

```typescript
// 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 `dark` class enables `dark:` prefixed utility classes
- **Custom styling**: The `theme-mode` attribute allows CSS selectors like `[theme-mode="dark"]` for styled-components or CSS modules
- **OS-specific styling**: The `os` attribute enables platform-specific tweaks (macOS, Windows, Linux)

### Persisting Theme Preferences

User preferences survive application restarts through IPC communication with the main process:

```typescript
// 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`](https://github.com/cherryhq/cherry-studio/blob/main/src/renderer/src/context/AntdProvider.tsx)) bridges the theme context with Ant Design's ConfigProvider:

```typescript
// 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`](https://github.com/cherryhq/cherry-studio/blob/main/src/renderer/src/context/CodeStyleProvider.tsx)) maps the current theme to appropriate Shiki themes:

```typescript
// 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:

```typescript
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`](https://github.com/cherryhq/cherry-studio/blob/main/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):

```typescript
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 `ThemeProvider` context in [`src/renderer/src/context/ThemeProvider.tsx`](https://github.com/cherryhq/cherry-studio/blob/main/src/renderer/src/context/ThemeProvider.tsx) that manages both user preferences and active theme state.
- **Dual State Management**: The system distinguishes between `settedTheme` (user's saved choice: light/dark/system) and `actualTheme` (currently applied: light/dark), enabling proper system-preference fallback.
- **DOM Integration**: The provider updates `<body>` attributes (`theme-mode`, `os`) and toggles the `dark` class 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`](https://github.com/cherryhq/cherry-studio/blob/main/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`](https://github.com/cherryhq/cherry-studio/blob/main/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.