# React Component Theming: How Plane Implements Dark/Light Modes with CSS Variables and Tailwind

> Discover how Plane implements React component theming for dark/light modes using CSS variables and Tailwind. Learn their innovative approach to style switching.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-22

---

**Plane uses a CSS-variable architecture combined with a custom Tailwind variant to enable automatic dark/light mode switching without duplicating component styles.**

The open-source project management platform Plane (makeplane/plane) implements a robust theming system that allows React components to seamlessly adapt between dark and light modes. By combining CSS custom properties with Tailwind's variant system, the codebase eliminates hard-coded color values and enables theme switching through data attributes. This article explains how Plane's React component theming with CSS variables and Tailwind creates a maintainable, token-based design system.

## The Foundation: CSS Variables and Custom Tailwind Variants

Plane's theming architecture centers on a single configuration file that bridges CSS variables and Tailwind's utility system. In [`packages/tailwind-config/variables.css`](https://github.com/makeplane/plane/blob/main/packages/tailwind-config/variables.css), the team defines not only the color tokens but also the mechanism for triggering dark mode styles.

### Defining the Dark Variant

The file introduces a custom Tailwind variant that instructs the compiler when to emit dark-mode styles:

```css
@custom-variant dark (&:where([data-theme*="dark"], [data-theme*="dark"] *));

```

This directive tells Tailwind that any class prefixed with `dark:` should only apply when an element—or its ancestor—carries a `data-theme="dark"` attribute. This approach provides more explicit control than Tailwind's default dark mode strategies, allowing Plane to support multiple theme variants including high-contrast modes through the same mechanism.

### Token-Based Color System

All visual properties in Plane are expressed as semantic CSS variables rather than static color values. The [`variables.css`](https://github.com/makeplane/plane/blob/main/variables.css) file defines tokens such as `--background-color-canvas`, `--border-color-subtle`, and `--text-color-primary` within separate `@variant dark` blocks. When the `data-theme` attribute changes, these variables update automatically, cascading new color values to every component reference without requiring JavaScript re-renders or class swaps.

## Theme Management with next-themes

Plane leverages the `next-themes` library to handle theme state persistence and application, creating a seamless bridge between user preferences and the CSS variable system.

### The ThemeSwitch Component

The UI for theme selection lives in [`apps/web/core/components/core/theme/theme-switch.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/core/theme/theme-switch.tsx). This component renders a dropdown that allows users to select between light, dark, and system preferences:

```tsx
export function ThemeSwitch(props: Props) {
  const { value, onChange } = props;
  const { t } = useTranslation();

  return (
    <CustomSelect
      value={value}
      label={value ? <.../> : t("select_your_theme")}
      onChange={onChange}
      buttonClassName="border border-subtle-1"
      placement="bottom-end"
      input
    >
      {THEME_OPTIONS.map(themeOption => (
        <CustomSelect.Option key={themeOption.value} value={themeOption}>
          <div className="flex items-center gap-2">…{t(themeOption.key)}</div>
        </CustomSelect.Option>
      ))}
    </CustomSelect>
  );
}

```

### Runtime Theme Application

When a user selects a theme, `next-themes` updates the `<html>` element's `data-theme` attribute to either `"light"` or `"dark"`. This single DOM change triggers the CSS custom variant system, causing Tailwind to activate the appropriate `@variant dark` styles throughout the entire application. The theme preference is automatically persisted to `localStorage` and hydrated on subsequent page loads.

## Component Implementation Patterns

React components in Plane never reference raw color values. Instead, they use semantic Tailwind utilities that map directly to the CSS variables defined in the configuration.

### Using Semantic Utility Classes

For example, in [`packages/propel/src/toast/toast.tsx`](https://github.com/makeplane/plane/blob/main/packages/propel/src/toast/toast.tsx), components apply theme-aware classes that resolve to CSS variables:

```tsx
<div data-theme={theme} className="inline-block">
  {/* Toast content using bg-canvas, text-primary, etc. */}
</div>

```

The `data-theme` attribute on this container ensures the correct variable values are applied, while classes like `bg-canvas` resolve to `background-color: var(--background-color-canvas);`. This abstraction means components remain agnostic about which specific theme is active.

## Practical Implementation Examples

### Creating a Theme-Aware Button

Components use semantic utility classes that automatically adapt to the active theme:

```tsx
export function MyButton() {
  return (
    <button className="bg-canvas text-primary border-subtle-1 hover:bg-layer-2">
      Click me
    </button>
  );
}

```

- `bg-canvas` maps to `var(--background-color-canvas)`
- `text-primary` maps to `var(--text-color-primary)`  
- `border-subtle-1` maps to `var(--border-color-subtle-1)`

When `data-theme="dark"` is present on the HTML element, these variables reference the dark-mode values defined in the `@variant dark` block of [`variables.css`](https://github.com/makeplane/plane/blob/main/variables.css).

### Building a Theme Toggle

Implementing a toggle button requires only the `next-themes` hook:

```tsx
import { useTheme } from "next-themes";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme(); // theme = "light" | "dark" | "system"

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Toggle to {theme === "dark" ? "light" : "dark"}
    </button>
  );
}

```

### Creating Reusable Layout Components

Higher-level components leverage the same token system for consistent styling:

```tsx
export function Card({ children }: { children: ReactNode }) {
  return (
    <div className="rounded-md bg-surface-1 border border-subtle-1 p-4 shadow-raised-100">
      {children}
    </div>
  );
}

```

This card component uses `bg-surface-1` and `border-subtle-1`, which automatically adjust their actual color values based on the current theme without additional logic.

## Summary

- **CSS variables** defined in [`packages/tailwind-config/variables.css`](https://github.com/makeplane/plane/blob/main/packages/tailwind-config/variables.css) provide the foundational color tokens for both light and dark modes.
- **Custom Tailwind variant** `@custom-variant dark` enables the `dark:` prefix to respond specifically to `[data-theme*="dark"]` attributes.
- **next-themes** manages theme state, persists user preferences to localStorage, and applies the `data-theme` attribute to the HTML element.
- **Semantic utility classes** like `bg-canvas` and `text-primary` allow components to remain theme-agnostic while automatically adapting to color changes.
- **Zero component duplication** is required for theming—adding new themes only requires adding new variable definitions in the CSS configuration.

## Frequently Asked Questions

### How does Plane detect dark mode without using Tailwind's default darkMode: 'class' strategy?

Plane defines a custom variant in [`packages/tailwind-config/variables.css`](https://github.com/makeplane/plane/blob/main/packages/tailwind-config/variables.css) using `@custom-variant dark` that specifically targets `[data-theme*="dark"]` attributes. This provides more explicit control than Tailwind's default class-based approach and allows the system to support multiple theme variants (including high-contrast) through the same mechanism.

### Can I add a custom theme (like high-contrast) without modifying React components?

Yes. Because components reference semantic tokens (such as `bg-canvas` and `text-primary`) rather than specific color values, you can add new themes by only modifying [`packages/tailwind-config/variables.css`](https://github.com/makeplane/plane/blob/main/packages/tailwind-config/variables.css). Adding a new `@variant` block with different CSS variable values immediately applies across the entire UI without touching any React code.

### Why does Plane use CSS variables instead of Tailwind's default color palette?

CSS variables enable runtime theme switching without recompiling Tailwind classes or shipping multiple CSS bundles. By changing a single `data-theme` attribute on the HTML element, the entire application updates instantly. This approach also supports dynamictheme adjustments and system-level preferences that static Tailwind classes cannot provide.

### How is the theme preference persisted across page reloads?

The `ThemeSwitch` component integrates with `next-themes`, which automatically stores the selected theme in localStorage and hydrates the `data-theme` attribute during the initial render. This ensures the user's preference remains active across sessions without requiring additional state management code in individual components.