Skip to main content
WebEditor includes a sophisticated theme system that automatically detects and applies the user’s preferred color scheme while allowing manual overrides.

Theme modes

WebEditor supports three theme modes:

Light mode

Fixed light color scheme

Dark mode

Fixed dark color scheme

Auto mode

Follows system preferences

useTheme hook

The useTheme hook provides theme management functionality:
Hook interface
export type Theme = 'light' | 'dark' | 'auto';

export function useTheme(): {
  theme: Theme;
  resolvedTheme: 'light' | 'dark';
  setTheme: (theme: Theme) => void;
  toggleTheme: () => void;
}

Return values

The current theme setting ('light', 'dark', or 'auto')
const { theme } = useTheme();
console.log(theme); // 'auto'

Hook implementation

The hook is implemented in ~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:

Initial theme detection

~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:11-25
const [theme, setThemeState] = useState<Theme>(() => {
  // Check if we're in a browser environment
  if (typeof window === 'undefined') {
    return 'auto';
  }

  // First check localStorage for user preference
  const savedTheme = localStorage.getItem('webeditor-theme') as Theme | null;
  if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark' || savedTheme === 'auto')) {
    return savedTheme;
  }

  // Default to auto mode
  return 'auto';
});
The theme preference is persisted to localStorage under the key webeditor-theme, so it survives page reloads.

Resolved theme calculation

~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:27-37
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
  if (typeof window === 'undefined') {
    return 'light';
  }

  if (theme === 'auto') {
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }

  return theme === 'dark' ? 'dark' : 'light';
});

System preference listener

~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:58-92
useEffect(() => {
  if (typeof window === 'undefined') return;

  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

  const handleChange = (e: MediaQueryListEvent) => {
    if (theme === 'auto') {
      setResolvedTheme(e.matches ? 'dark' : 'light');
    }
  };

  // Set initial resolved theme based on current preference
  if (theme === 'auto') {
    setResolvedTheme(mediaQuery.matches ? 'dark' : 'light');
  } else {
    setResolvedTheme(theme === 'dark' ? 'dark' : 'light');
  }

  // Use the newer addEventListener API if available, fallback to addListener
  if (mediaQuery.addEventListener) {
    mediaQuery.addEventListener('change', handleChange);
  } else {
    // @ts-ignore - Legacy API
    mediaQuery.addListener(handleChange);
  }

  return () => {
    if (mediaQuery.removeEventListener) {
      mediaQuery.removeEventListener('change', handleChange);
    } else {
      // @ts-ignore - Legacy API
      mediaQuery.removeListener(handleChange);
    }
  };
}, [theme]);
The hook automatically listens for changes to the system’s color scheme preference and updates the resolved theme accordingly when in auto mode.

Applying theme to DOM

~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:95-105
useEffect(() => {
  if (typeof document !== 'undefined') {
    const root = document.documentElement;

    // Remove existing theme classes
    root.classList.remove('dark', 'light');

    // Add the resolved theme class
    root.classList.add(resolvedTheme);
  }
}, [resolvedTheme]);

Usage in WebEditor

The WebEditor component initializes theme detection automatically:
~/workspace/source/packages/webeditor/src/editor/index.tsx:70-72
export function WebEditor(props: { value?: string; editable?: boolean; onChange?: (doc: string) => void }) {
  // Initialize theme detection and management - automatically detects browser's preferred theme
  // and applies dark/light mode classes to the document root
  const { theme, resolvedTheme } = useTheme();
  
  // Rest of component...
}
You don’t need to do anything to enable theme support - it’s automatically initialized when you use the WebEditor component.

Customizing theme behavior

You can create a theme toggle button:
Example theme toggle
import { useTheme } from '@your-package/webeditor';
import { Moon, Sun, Monitor } from 'lucide-react';

function ThemeToggle() {
  const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
  
  const Icon = theme === 'auto' ? Monitor : resolvedTheme === 'dark' ? Moon : Sun;
  
  return (
    <button onClick={toggleTheme} title={`Current theme: ${theme}`}>
      <Icon className="w-5 h-5" />
    </button>
  );
}

CSS variables and styling

WebEditor uses CSS custom properties for theming. These are typically defined in your CSS:
Example theme variables
:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 3.9%;
  --primary: 0 0% 9%;
  --primary-foreground: 0 0% 98%;
  /* ... more variables */
}

.dark {
  --background: 0 0% 3.9%;
  --foreground: 0 0% 98%;
  --card: 0 0% 3.9%;
  --card-foreground: 0 0% 98%;
  --primary: 0 0% 98%;
  --primary-foreground: 0 0% 9%;
  /* ... more variables */
}
WebEditor components use these CSS variables:
  • --background - Main background color
  • --foreground - Main text color
  • --card - Card background
  • --card-foreground - Card text
  • --popover - Popover/menu background
  • --popover-foreground - Popover text
  • --primary - Primary accent color
  • --primary-foreground - Primary accent text
  • --secondary - Secondary accent color
  • --secondary-foreground - Secondary accent text
  • --muted - Muted background
  • --muted-foreground - Muted text
  • --accent - Accent background (hover states)
  • --accent-foreground - Accent text
  • --border - Border color
  • --ring - Focus ring color

Server-side rendering

The theme hook is SSR-safe:
if (typeof window === 'undefined') {
  return 'auto'; // Safe default for SSR
}
During server-side rendering, the hook returns sensible defaults and skips browser-specific operations.

Theme persistence

Theme preferences are saved to localStorage:
~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:39-45
const setTheme = (newTheme: Theme) => {
  setThemeState(newTheme);

  if (typeof window !== 'undefined') {
    localStorage.setItem('webeditor-theme', newTheme);
  }
};

Theme toggle function

The toggle function cycles through all three modes:
~/workspace/source/packages/webeditor/src/hooks/use-theme.ts:47-56
const toggleTheme = () => {
  if (theme === 'auto') {
    setTheme('light');
  } else if (theme === 'light') {
    setTheme('dark');
  } else {
    setTheme('auto');
  }
};
The toggle cycle is: auto → light → dark → auto

Best practices

Use auto mode by default

Auto mode respects user preferences and adapts to system settings

Provide manual override

Give users a way to override auto mode if they prefer

Persist preferences

The hook automatically saves to localStorage, ensuring consistency

Test both themes

Ensure your custom components work in both light and dark modes

Next steps

Overview

Return to the architecture overview

API Reference

Explore the complete useTheme API

Build docs developers (and LLMs) love