Skip to main content

ADR-008: Implement Dark/Light Themes

Date: 2025-10-06 Status: Accepted Deciders: Development Team Tags: ui, themes, ux

Context​

The IDE needs:

  • Professional VS Code-like appearance
  • Support for dark and light modes
  • User preference persistence
  • System theme detection option
  • Consistent theming across all components

Decision​

Implement dark and light themes using Chakra UI's color mode system with VS Code-inspired color tokens.

Rationale​

Why Chakra UI Color Mode​

  1. Built-in: Native dark/light mode support
  2. Semantic Tokens: Theme-aware color values
  3. Auto-Persist: LocalStorage persistence included
  4. System Sync: Optional OS theme detection
  5. Easy Toggle: Simple toggleColorMode() API
  6. SSR Support: Works with server-side rendering
  7. No Flash: Prevents theme flash on load

Design Goals​

  • VS Code Aesthetic: Match familiar editor look
  • Professional: Subtle colors, good contrast
  • Accessible: WCAG AA compliance minimum
  • Consistent: Same theme across all panels
  • Customizable: Users can override colors

Implementation​

Theme Configuration​

// src/theme/chakra-theme.ts
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';

const config: ThemeConfig = {
initialColorMode: 'dark',
useSystemColorMode: false,
};

const theme = extendTheme({
config,

// VS Code color palette
colors: {
brand: {
50: '#e3f2fd',
100: '#bbdefb',
200: '#90caf9',
300: '#64b5f6',
400: '#42a5f5',
500: '#2196f3', // Primary blue
600: '#1e88e5',
700: '#1976d2',
800: '#1565c0',
900: '#0d47a1',
},
},

// Semantic tokens for theme-aware colors
semanticTokens: {
colors: {
// editor backgrounds
'editor.bg': {
default: '#1e1e1e', // VS Code dark
_light: '#ffffff', // VS Code light
},
'editor.sidebar': {
default: '#252526',
_light: '#f3f3f3',
},
'editor.activityBar': {
default: '#333333',
_light: '#2c2c2c',
},

// Borders
'editor.border': {
default: '#3e3e42',
_light: '#e0e0e0',
},

// Text colors
'editor.text': {
default: '#cccccc',
_light: '#333333',
},
'editor.textMuted': {
default: '#858585',
_light: '#6c6c6c',
},

// Interactive elements
'editor.hover': {
default: '#2a2d2e',
_light: '#e8e8e8',
},
'editor.selection': {
default: '#264f78',
_light: '#add6ff',
},
'editor.focus': {
default: '#007acc',
_light: '#0066bf',
},

// Status colors
'status.error': {
default: '#f48771',
_light: '#e51400',
},
'status.warning': {
default: '#cca700',
_light: '#bf8803',
},
'status.info': {
default: '#75beff',
_light: '#1a85ff',
},
'status.success': {
default: '#89d185',
_light: '#388a34',
},

// terminal colors
'terminal.bg': {
default: '#1e1e1e',
_light: '#ffffff',
},
'terminal.fg': {
default: '#cccccc',
_light: '#333333',
},
'terminal.cursor': {
default: '#ffffff',
_light: '#000000',
},
},
},

// Component style overrides
components: {
Button: {
variants: {
ghost: {
_hover: {
bg: 'editor.hover',
},
},
},
},
Tabs: {
variants: {
line: {
tab: {
borderBottom: '2px solid transparent',
_selected: {
color: 'editor.focus',
borderColor: 'editor.focus',
},
_hover: {
bg: 'editor.hover',
},
},
},
},
},
},

// Global styles
styles: {
global: (props: any) => ({
body: {
bg: props.colorMode === 'dark' ? '#1e1e1e' : '#ffffff',
color: props.colorMode === 'dark' ? '#cccccc' : '#333333',
},
}),
},
});

export default theme;

Theme Toggle Component​

// src/components/ThemeToggle.tsx
import { IconButton, useColorMode } from '@chakra-ui/react';
import { FiSun, FiMoon } from 'react-icons/fi';

export default function ThemeToggle() {
const { colorMode, toggleColorMode } = useColorMode();

return (
<IconButton
icon={colorMode === 'light' ? <FiMoon /> : <FiSun />}
onClick={toggleColorMode}
aria-label={`Switch to ${colorMode === 'light' ? 'dark' : 'light'} mode`}
size="sm"
variant="ghost"
_hover={{ bg: 'editor.hover' }}
/>
);
}

Monaco editor Theme Sync​

// src/hooks/useMonacoTheme.ts
import { useColorMode } from '@chakra-ui/react';
import { useEffect } from 'react';
import * as monaco from 'monaco-editor';

export function useMonacoTheme() {
const { colorMode } = useColorMode();

useEffect(() => {
// Define custom VS Code theme for Monaco
monaco.editor.defineTheme('az1ai-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '6A9955' },
{ token: 'keyword', foreground: 'C586C0' },
{ token: 'string', foreground: 'CE9178' },
{ token: 'number', foreground: 'B5CEA8' },
{ token: 'type', foreground: '4EC9B0' },
{ token: 'function', foreground: 'DCDCAA' },
],
colors: {
'editor.background': '#1e1e1e',
'editor.foreground': '#cccccc',
'editorLineNumber.foreground': '#858585',
'editor.selectionBackground': '#264f78',
},
});

monaco.editor.defineTheme('az1ai-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'comment', foreground: '008000' },
{ token: 'keyword', foreground: '0000FF' },
{ token: 'string', foreground: 'A31515' },
{ token: 'number', foreground: '098658' },
],
colors: {
'editor.background': '#ffffff',
'editor.foreground': '#333333',
},
});

// Apply theme
monaco.editor.setTheme(colorMode === 'dark' ? 'az1ai-dark' : 'az1ai-light');
}, [colorMode]);
}

xterm.js Theme Sync​

// src/hooks/useterminalTheme.ts
import { useColorMode } from '@chakra-ui/react';
import { useEffect } from 'react';
import type { ITheme } from 'xterm';

export function useterminalTheme(terminal: terminal | null) {
const { colorMode } = useColorMode();

useEffect(() => {
if (!terminal) return;

const darkTheme: ITheme = {
background: '#1e1e1e',
foreground: '#cccccc',
cursor: '#ffffff',
cursorAccent: '#1e1e1e',
selectionBackground: '#264f78',
black: '#000000',
red: '#cd3131',
green: '#0dbc79',
yellow: '#e5e510',
blue: '#2472c8',
magenta: '#bc3fbc',
cyan: '#11a8cd',
white: '#e5e5e5',
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#e5e5e5',
};

const lightTheme: ITheme = {
background: '#ffffff',
foreground: '#333333',
cursor: '#000000',
cursorAccent: '#ffffff',
selectionBackground: '#add6ff',
black: '#000000',
red: '#cd3131',
green: '#00bc00',
yellow: '#949800',
blue: '#0451a5',
magenta: '#bc05bc',
cyan: '#0598bc',
white: '#555555',
brightBlack: '#666666',
brightRed: '#cd3131',
brightGreen: '#14ce14',
brightYellow: '#b5ba00',
brightBlue: '#0451a5',
brightMagenta: '#bc05bc',
brightCyan: '#0598bc',
brightWhite: '#a5a5a5',
};

terminal.options.theme = colorMode === 'dark' ? darkTheme : lightTheme;
}, [terminal, colorMode]);
}

User Preferences​

// src/store/settingsStore.ts
interface SettingsStore {
theme: 'dark' | 'light' | 'system';
setTheme: (theme: 'dark' | 'light' | 'system') => void;
}

export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'dark',
setTheme: (theme) => {
set({ theme });

if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const { setColorMode } = useColorMode.getState();
setColorMode(prefersDark ? 'dark' : 'light');
}
},
}),
{ name: 'settings-storage' }
)
);

Settings Panel​

// src/components/Settings/ThemeSettings.tsx
export function ThemeSettings() {
const { colorMode, setColorMode } = useColorMode();
const { theme, setTheme } = useSettingsStore();

return (
<FormControl>
<FormLabel>Color Theme</FormLabel>
<RadioGroup
value={theme}
onChange={(value) => {
setTheme(value as 'dark' | 'light' | 'system');
if (value !== 'system') {
setColorMode(value as 'dark' | 'light');
}
}}
>
<Stack>
<Radio value="dark">Dark</Radio>
<Radio value="light">Light</Radio>
<Radio value="system">Use System Theme</Radio>
</Stack>
</RadioGroup>
</FormControl>
);
}

Color Palette​

Dark Theme​

TokenColorUsage
editor.bg#1e1e1eMain background
editor.sidebar#252526Sidebar background
editor.border#3e3e42Borders
editor.text#ccccccPrimary text
editor.textMuted#858585Secondary text
editor.focus#007accFocus indicators

Light Theme​

TokenColorUsage
editor.bg#ffffffMain background
editor.sidebar#f3f3f3Sidebar background
editor.border#e0e0e0Borders
editor.text#333333Primary text
editor.textMuted#6c6c6cSecondary text
editor.focus#0066bfFocus indicators

Accessibility​

Contrast Ratios​

All color combinations meet WCAG AA standards:

  • Dark Mode: Minimum 4.5:1 for text
  • Light Mode: Minimum 4.5:1 for text
  • Interactive Elements: Minimum 3:1

Testing​

// Check contrast ratio
import { contrast } from 'wcag-contrast';

const darkBg = '#1e1e1e';
const darkText = '#cccccc';
console.log(contrast(darkBg, darkText)); // Should be >= 4.5

References​