How Clash Nyanpasu Implements Material You Theming and Dynamic Color Systems

Clash Nyanpasu generates a complete Material Design 3 theme from a user-defined seed color using Google's Material Color Utilities, then distributes the dynamic palette throughout the application via React context and CSS custom properties.

Clash Nyanpasu is a modern GUI proxy client built with React and Tauri that leverages Google's Material You (Material Design 3) specification for its theming system. The implementation centers on dynamic color generation, where a single seed color propagates into a full tonal palette that adapts to both user preferences and operating system theme changes.

Core Architecture of Material You Theming

The theming system is orchestrated through a custom React hook that bridges user settings, Material Design algorithms, and the component tree.

The use-custom-theme.tsx Hook

Located at frontend/nyanpasu/src/components/layout/use-custom-theme.tsx, this hook serves as the central nervous system for theme generation. It performs five critical functions:

  1. Reads the user-defined seed color from the theme_color setting, defaulting to #1867c0 if undefined
  2. Invokes the Material You generator via createMDYTheme from the workspace package @nyanpasu/ui
  3. Provides the theme to the React tree through MUI's <ThemeProvider>
  4. Synchronizes OS-level color schemes using Tauri's appWindow.theme() API and the useColorScheme hook
  5. Updates CSS custom properties on the <html> element (e.g., --background-color, --primary-main) for non-MUI components

The hook uses useMemo to recompute the theme only when the seed color or color mode changes, ensuring efficient updates without full page reloads.

Theme Generation with createMDYTheme

The createMDYTheme function (implemented in the @nyanpasu/ui workspace package) acts as the bridge between Google's color science and MUI's theming system:

// Conceptual implementation based on source analysis
import { CorePalette, argbFromHex } from '@material/material-color-utilities';

const createMDYTheme = (seedColor: string, fontFamily: string) => {
  // Convert hex to ARGB and generate tonal palettes
  const seedArgb = argbFromHex(seedColor);
  const corePalette = CorePalette.of(seedArgb);
  
  // Map Material You tonal palettes to MUI palette structure
  return createMuiTheme({
    palette: {
      primary: generateTonalPalette(corePalette.a1),
      secondary: generateTonalPalette(corePalette.a2),
      tertiary: generateTonalPalette(corePalette.a3),
      error: generateTonalPalette(corePalette.error),
    },
    typography: { fontFamily }
  });
};

This function relies on @material/material-color-utilities (declared in frontend/ui/package.json) to execute the Material You algorithm, which extracts hue, chroma, and tone values to generate harmonious color families.

Dynamic Color System Implementation

Seed Color and Tonal Palettes

The dynamic color system originates from a single seed color stored in the application's settings. When the user selects a new accent color:

  1. The setting updates via the useSetting('theme_color') hook
  2. The new hex value flows into createMDYTheme
  3. @material/material-color-utilities calculates tonal palettes for primary, secondary, tertiary, and error containers
  4. These palettes map to MUI's palette.primary, palette.secondary, etc.

Each tonal palette contains 13 tones (from 0% to 100% luminance), ensuring accessible contrast ratios across light and dark modes.

CSS Custom Properties Synchronization

To support components outside MUI's theme context (such as Tailwind-styled elements), the system synchronizes CSS custom properties:

// From use-custom-theme.tsx
const applyRootStyleVar = (mode: 'light' | 'dark', theme: Theme) => {
  const root = document.documentElement;
  root.style.setProperty('--background-color', theme.palette.background.default);
  root.style.setProperty('--primary-main', theme.palette.primary.main);
  root.style.setProperty('--primary-contrast', theme.palette.primary.contrastText);
  // ... additional mappings
};

This dual-delivery system (React context + CSS variables) ensures consistent theming across the entire application stack.

OS-Level Theme Synchronization

Tauri Integration for System Theme Detection

The application respects the operating system's color scheme preference through Tauri's window API:

// Located in scheme-provider.tsx and use-custom-theme.tsx
import { appWindow } from '@tauri-apps/api/webviewWindow';

// Initialize theme from OS
const systemTheme = await appWindow.theme(); // returns 'light' | 'dark'

// Listen for changes
const unlisten = await appWindow.onThemeChanged((event) => {
  setMode(event.payload); // Update Jotai atom and MUI scheme
});

This integration, combined with the scheme-provider.tsx component, ensures the application transitions seamlessly when the user toggles system dark mode.

Practical Implementation Examples

Accessing Theme Values in Components

import { Card, CardContent, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';

export const InfoCard = () => {
  const theme = useTheme();               // <- receives Material You colours
  return (
    <Card sx={{ backgroundColor: theme.palette.primary.main }}>
      <CardContent>
        <Typography color="primary.contrastText">
          Material You themed card
        </Typography>
      </CardContent>
    </Card>
  );
};

Updating the Seed Color Programmatically

import { useSetting } from '@nyanpasu/interface';
import { Slider } from '@mui/material';
import { useState } from 'react';

export const ThemeColorPicker = () => {
  const { value: current, setValue } = useSetting('theme_color');
  const [hex, setHex] = useState(current ?? '#1867c0');

  const handleChange = (_, newHex: string) => {
    setHex(newHex);
    setValue(newHex);                     // triggers re‑generation of the MDY theme
  };

  return <Slider value={hex} onChange={handleChange} />;
};

Synchronizing with System Theme Changes

import { useEffect } from 'react';
import { appWindow } from '@tauri-apps/api/webviewWindow';
import { useSetAtom } from 'jotai';
import { themeMode as themeModeAtom } from '@/store';

export const SyncSystemTheme = () => {
  const setMode = useSetAtom(themeModeAtom);

  useEffect(() => {
    appWindow.theme().then(m => m && setMode(m));      // initialise
    const unlisten = appWindow.onThemeChanged(e => setMode(e.payload));
    return () => unlisten.then(fn => fn());
  }, []);
  return null;
};

Summary

  • Central Hook: The use-custom-theme.tsx hook in frontend/nyanpasu/src/components/layout/ orchestrates the entire theming system by reading user settings and generating MUI themes.
  • Material You Integration: The @nyanpasu/ui workspace package implements createMDYTheme, which leverages @material/material-color-utilities to generate tonal palettes from a single seed color.
  • Dynamic Updates: The system uses useMemo to recompute themes when the theme_color setting changes, applying updates via React context and CSS custom properties without page reloads.
  • OS Synchronization: Tauri's appWindow.theme() API and onThemeChanged event listener ensure the application respects and reacts to system-level light/dark mode changes.
  • Dual Delivery: Themes are distributed through both MUI's <ThemeProvider> and synchronized CSS variables (--primary-main, etc.), ensuring consistent styling across MUI components and Tailwind-styled elements.

Frequently Asked Questions

How does Clash Nyanpasu generate a full color palette from a single hex code?

Clash Nyanpasu uses Google's @material/material-color-utilities library to execute the Material You algorithm. When a user selects a seed color (e.g., #1867c0), the createMDYTheme function converts this to ARGB format and passes it to CorePalette.of(), which mathematically derives hue, chroma, and tone values to generate 13-tone palettes for primary, secondary, tertiary, and error colors.

What happens when the user changes the theme color while the app is running?

The change triggers an immediate update through the use-custom-theme.tsx hook. Because the theme generation is wrapped in useMemo with dependencies on theme_color and the current color mode, React recomputes the MUI theme object instantly. The hook then updates the <ThemeProvider> context and synchronizes CSS custom properties on the <html> element, causing the entire UI to repaint with the new palette without requiring a page reload.

How does the application handle system dark mode or light mode changes?

The application listens for OS-level theme changes through Tauri's appWindow.onThemeChanged event listener, implemented in scheme-provider.tsx and the custom theme hook. When the system theme changes, the event payload updates a Jotai atom (themeModeAtom) and MUI's useColorScheme hook, which triggers the theme generator to switch between light and dark palette variants while maintaining the same seed color and tonal relationships.

Can non-MUI components access the Material You color values?

Yes. While MUI components consume the theme through React context, the system exposes the same color values as CSS custom properties on the root <html> element. The applyRootStyleVar function in use-custom-theme.tsx synchronizes variables such as --primary-main, --background-color, and --primary-contrast whenever the theme updates. This allows Tailwind CSS and custom styled components to reference these values using var(--primary-main) and remain visually consistent with the Material You palette.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →