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​
- Built-in: Native dark/light mode support
- Semantic Tokens: Theme-aware color values
- Auto-Persist: LocalStorage persistence included
- System Sync: Optional OS theme detection
- Easy Toggle: Simple
toggleColorMode()API - SSR Support: Works with server-side rendering
- 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​
| Token | Color | Usage |
|---|---|---|
editor.bg | #1e1e1e | Main background |
editor.sidebar | #252526 | Sidebar background |
editor.border | #3e3e42 | Borders |
editor.text | #cccccc | Primary text |
editor.textMuted | #858585 | Secondary text |
editor.focus | #007acc | Focus indicators |
Light Theme​
| Token | Color | Usage |
|---|---|---|
editor.bg | #ffffff | Main background |
editor.sidebar | #f3f3f3 | Sidebar background |
editor.border | #e0e0e0 | Borders |
editor.text | #333333 | Primary text |
editor.textMuted | #6c6c6c | Secondary text |
editor.focus | #0066bf | Focus 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