Skip to content

Commit 023aad4

Browse files
committed
Fix theme customizer to support both light and dark modes
Updates: - Add dark color scales to ACCENT_COLORS and GRAY_COLORS lookup tables - Restructure color data to include both light and dark step values - Update generateCSS to output three CSS blocks: 1. :root with light mode values 2. @media (prefers-color-scheme: dark) for automatic dark mode 3. .dark class for explicit dark mode - Update Swatch components to use light.step9/step11 - This fixes automatic dark mode support when using custom Radix themes https://claude.ai/code/session_014nSucTSwQuGmUfrfoBM5Zy
1 parent 09b60f7 commit 023aad4

1 file changed

Lines changed: 110 additions & 58 deletions

File tree

demo/src/theme-customizer.tsx

Lines changed: 110 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -33,52 +33,49 @@ function hsl(hex: string) {
3333

3434
/* ── Radix accent color data (from @radix-ui/colors step hex values) ─ */
3535
interface AccentData {
36-
step3: string;
37-
step8: string;
38-
step9: string;
39-
step10: string;
40-
step11: string;
36+
light: {step3: string; step8: string; step9: string; step10: string; step11: string};
37+
dark: {step3: string; step9: string; step10: string; step11: string; step12: string};
4138
/** true if step-9 needs dark text (light bg accent like amber/yellow) */
4239
darkContrast?: boolean;
4340
}
4441

4542
const ACCENT_COLORS: Record<string, AccentData> = {
46-
tomato: {step3: '#feebe7', step8: '#ec8e7b', step9: '#e54d2e', step10: '#dd4425', step11: '#d13415'},
47-
red: {step3: '#feebec', step8: '#eb8e90', step9: '#e5484d', step10: '#dc3e42', step11: '#ce2c31'},
48-
ruby: {step3: '#feeaed', step8: '#e592a3', step9: '#e54666', step10: '#dc3b5d', step11: '#ca244d'},
49-
crimson: {step3: '#ffe9f0', step8: '#e093b2', step9: '#e93d82', step10: '#df3478', step11: '#cb1d63'},
50-
pink: {step3: '#fee9f5', step8: '#dd93c2', step9: '#d6409f', step10: '#cf3897', step11: '#c2298a'},
51-
plum: {step3: '#fbebfb', step8: '#cf91d8', step9: '#ab4aba', step10: '#a144af', step11: '#953ea3'},
52-
purple: {step3: '#f7edfe', step8: '#be93e4', step9: '#8e4ec6', step10: '#8347b9', step11: '#8145b5'},
53-
violet: {step3: '#f4f0fe', step8: '#aa99ec', step9: '#6e56cf', step10: '#654dc4', step11: '#6550b9'},
54-
iris: {step3: '#f0f1fe', step8: '#9b9ef0', step9: '#5b5bd6', step10: '#5151cd', step11: '#5753c6'},
55-
indigo: {step3: '#edf2fe', step8: '#8da4ef', step9: '#3e63dd', step10: '#3358d4', step11: '#3a5bc7'},
56-
blue: {step3: '#e6f4fe', step8: '#5eb1ef', step9: '#0090ff', step10: '#0588f0', step11: '#0d74ce'},
57-
cyan: {step3: '#def7f9', step8: '#3db9cf', step9: '#00a2c7', step10: '#0797b9', step11: '#107d98'},
58-
teal: {step3: '#e0f8f3', step8: '#53b9ab', step9: '#12a594', step10: '#0d9b8a', step11: '#008573'},
59-
jade: {step3: '#e6f7ed', step8: '#56ba9f', step9: '#29a383', step10: '#26997b', step11: '#208368'},
60-
green: {step3: '#e6f6eb', step8: '#5bb98b', step9: '#30a46c', step10: '#2b9a66', step11: '#218358'},
61-
grass: {step3: '#e9f6e9', step8: '#65ba74', step9: '#46a758', step10: '#3e9b4f', step11: '#2a7e3b'},
62-
orange: {step3: '#ffefd6', step8: '#ec9455', step9: '#f76b15', step10: '#ef5f00', step11: '#cc4e00'},
63-
amber: {step3: '#fff7c2', step8: '#e2a336', step9: '#ffc53d', step10: '#ffba18', step11: '#ab6400', darkContrast: true},
64-
yellow: {step3: '#fffab8', step8: '#d5ae39', step9: '#ffe629', step10: '#ffdc00', step11: '#9e6c00', darkContrast: true},
65-
lime: {step3: '#eef6d6', step8: '#8db654', step9: '#bdee63', step10: '#b0e64c', step11: '#5c7c2f', darkContrast: true},
66-
mint: {step3: '#ddf9f2', step8: '#4cbba5', step9: '#86ead4', step10: '#7de0cb', step11: '#027864', darkContrast: true},
67-
sky: {step3: '#e1f6fd', step8: '#60b3d7', step9: '#7ce2fe', step10: '#74daf8', step11: '#00749e', darkContrast: true},
68-
bronze: {step3: '#f6edea', step8: '#c2a499', step9: '#a18072', step10: '#957468', step11: '#7d5e54'},
69-
gold: {step3: '#f2f0e7', step8: '#b9a88d', step9: '#978365', step10: '#8c7a5e', step11: '#71624b'},
70-
brown: {step3: '#f6eee7', step8: '#cea37e', step9: '#ad7f58', step10: '#a07553', step11: '#815e46'},
43+
tomato: {light: {step3: '#feebe7', step8: '#ec8e7b', step9: '#e54d2e', step10: '#dd4425', step11: '#d13415'}, dark: {step3: '#391714', step9: '#e54d2e', step10: '#ec6142', step11: '#ff977d', step12: '#fbd3cb'}},
44+
red: {light: {step3: '#feebec', step8: '#eb8e90', step9: '#e5484d', step10: '#dc3e42', step11: '#ce2c31'}, dark: {step3: '#3b1219', step9: '#e5484d', step10: '#ec5d5e', step11: '#ff9592', step12: '#ffd1d9'}},
45+
ruby: {light: {step3: '#feeaed', step8: '#e592a3', step9: '#e54666', step10: '#dc3b5d', step11: '#ca244d'}, dark: {step3: '#3a141e', step9: '#e54666', step10: '#ec5a72', step11: '#ff949d', step12: '#fed2e1'}},
46+
crimson: {light: {step3: '#ffe9f0', step8: '#e093b2', step9: '#e93d82', step10: '#df3478', step11: '#cb1d63'}, dark: {step3: '#381525', step9: '#e93d82', step10: '#ee518a', step11: '#ff92ad', step12: '#fdd3e8'}},
47+
pink: {light: {step3: '#fee9f5', step8: '#dd93c2', step9: '#d6409f', step10: '#cf3897', step11: '#c2298a'}, dark: {step3: '#37172f', step9: '#d6409f', step10: '#de51a8', step11: '#ff8dcc', step12: '#fdd1ea'}},
48+
plum: {light: {step3: '#fbebfb', step8: '#cf91d8', step9: '#ab4aba', step10: '#a144af', step11: '#953ea3'}, dark: {step3: '#351a35', step9: '#ab4aba', step10: '#b658c4', step11: '#e796f3', step12: '#f4d4f4'}},
49+
purple: {light: {step3: '#f7edfe', step8: '#be93e4', step9: '#8e4ec6', step10: '#8347b9', step11: '#8145b5'}, dark: {step3: '#301c3b', step9: '#8e4ec6', step10: '#9a5cd0', step11: '#d19dff', step12: '#ecd9fa'}},
50+
violet: {light: {step3: '#f4f0fe', step8: '#aa99ec', step9: '#6e56cf', step10: '#654dc4', step11: '#6550b9'}, dark: {step3: '#291f43', step9: '#6e56cf', step10: '#7d66d9', step11: '#baa7ff', step12: '#e2ddfe'}},
51+
iris: {light: {step3: '#f0f1fe', step8: '#9b9ef0', step9: '#5b5bd6', step10: '#5151cd', step11: '#5753c6'}, dark: {step3: '#202248', step9: '#5b5bd6', step10: '#6e6ade', step11: '#b1a9ff', step12: '#e0dffe'}},
52+
indigo: {light: {step3: '#edf2fe', step8: '#8da4ef', step9: '#3e63dd', step10: '#3358d4', step11: '#3a5bc7'}, dark: {step3: '#182449', step9: '#3e63dd', step10: '#5472e4', step11: '#9eb1ff', step12: '#d6e1ff'}},
53+
blue: {light: {step3: '#e6f4fe', step8: '#5eb1ef', step9: '#0090ff', step10: '#0588f0', step11: '#0d74ce'}, dark: {step3: '#0d2847', step9: '#0090ff', step10: '#3b9eff', step11: '#70b8ff', step12: '#c2e6ff'}},
54+
cyan: {light: {step3: '#def7f9', step8: '#3db9cf', step9: '#00a2c7', step10: '#0797b9', step11: '#107d98'}, dark: {step3: '#082c36', step9: '#00a2c7', step10: '#23afd0', step11: '#4ccce6', step12: '#b6ecf7'}},
55+
teal: {light: {step3: '#e0f8f3', step8: '#53b9ab', step9: '#12a594', step10: '#0d9b8a', step11: '#008573'}, dark: {step3: '#0d2d2a', step9: '#12a594', step10: '#0eb39e', step11: '#0bd8b6', step12: '#adf0dd'}},
56+
jade: {light: {step3: '#e6f7ed', step8: '#56ba9f', step9: '#29a383', step10: '#26997b', step11: '#208368'}, dark: {step3: '#0f2e22', step9: '#29a383', step10: '#27b08b', step11: '#1fd8a4', step12: '#adf0d4'}},
57+
green: {light: {step3: '#e6f6eb', step8: '#5bb98b', step9: '#30a46c', step10: '#2b9a66', step11: '#218358'}, dark: {step3: '#132d21', step9: '#30a46c', step10: '#33b074', step11: '#3dd68c', step12: '#b1f1cb'}},
58+
grass: {light: {step3: '#e9f6e9', step8: '#65ba74', step9: '#46a758', step10: '#3e9b4f', step11: '#2a7e3b'}, dark: {step3: '#1b2a1e', step9: '#46a758', step10: '#53b365', step11: '#71d083', step12: '#c2f0c2'}},
59+
orange: {light: {step3: '#ffefd6', step8: '#ec9455', step9: '#f76b15', step10: '#ef5f00', step11: '#cc4e00'}, dark: {step3: '#331e0b', step9: '#f76b15', step10: '#ff801f', step11: '#ffa057', step12: '#ffe0c2'}},
60+
amber: {light: {step3: '#fff7c2', step8: '#e2a336', step9: '#ffc53d', step10: '#ffba18', step11: '#ab6400'}, dark: {step3: '#302008', step9: '#ffc53d', step10: '#ffd60a', step11: '#ffca16', step12: '#ffe7b3'}, darkContrast: true},
61+
yellow: {light: {step3: '#fffab8', step8: '#d5ae39', step9: '#ffe629', step10: '#ffdc00', step11: '#9e6c00'}, dark: {step3: '#2d2305', step9: '#ffe629', step10: '#ffff57', step11: '#f5e147', step12: '#f6eeb4'}, darkContrast: true},
62+
lime: {light: {step3: '#eef6d6', step8: '#8db654', step9: '#bdee63', step10: '#b0e64c', step11: '#5c7c2f'}, dark: {step3: '#1f2917', step9: '#bdee63', step10: '#d4ff70', step11: '#bde56c', step12: '#e3f7ba'}, darkContrast: true},
63+
mint: {light: {step3: '#ddf9f2', step8: '#4cbba5', step9: '#86ead4', step10: '#7de0cb', step11: '#027864'}, dark: {step3: '#092c2b', step9: '#86ead4', step10: '#a8f5e5', step11: '#58d5ba', step12: '#c4f5e1'}, darkContrast: true},
64+
sky: {light: {step3: '#e1f6fd', step8: '#60b3d7', step9: '#7ce2fe', step10: '#74daf8', step11: '#00749e'}, dark: {step3: '#112840', step9: '#7ce2fe', step10: '#a8eeff', step11: '#75c7f0', step12: '#c2f3ff'}, darkContrast: true},
65+
bronze: {light: {step3: '#f6edea', step8: '#c2a499', step9: '#a18072', step10: '#957468', step11: '#7d5e54'}, dark: {step3: '#262220', step9: '#a18072', step10: '#ae8c7e', step11: '#d4b3a5', step12: '#ede0d9'}},
66+
gold: {light: {step3: '#f2f0e7', step8: '#b9a88d', step9: '#978365', step10: '#8c7a5e', step11: '#71624b'}, dark: {step3: '#24231f', step9: '#978365', step10: '#a39073', step11: '#cbb99f', step12: '#e8e2d9'}},
67+
brown: {light: {step3: '#f6eee7', step8: '#cea37e', step9: '#ad7f58', step10: '#a07553', step11: '#815e46'}, dark: {step3: '#28211d', step9: '#ad7f58', step10: '#b88c67', step11: '#dbb594', step12: '#f2e1ca'}},
7168
};
7269

7370
const ACCENT_NAMES = Object.keys(ACCENT_COLORS);
7471

75-
const GRAY_COLORS: Record<string, {step1: string; step3: string; step6: string; step11: string; step12: string}> = {
76-
gray: {step1: '#fcfcfc', step3: '#f0f0f0', step6: '#d9d9d9', step11: '#646464', step12: '#202020'},
77-
mauve: {step1: '#fdfcfd', step3: '#f1eef1', step6: '#d9d5dc', step11: '#645f6b', step12: '#211f26'},
78-
slate: {step1: '#fcfcfd', step3: '#eef0f3', step6: '#d3d8e0', step11: '#5b6373', step12: '#1a2030'},
79-
sage: {step1: '#fbfdfc', step3: '#eff1f0', step6: '#d4dad7', step11: '#5f6563', step12: '#1a211e'},
80-
olive: {step1: '#fcfdfc', step3: '#eff1ef', step6: '#d5d8d3', step11: '#60655f', step12: '#1d211c'},
81-
sand: {step1: '#fdfdfc', step3: '#f1f0ef', step6: '#d7d5d0', step11: '#63635e', step12: '#21201c'},
72+
const GRAY_COLORS: Record<string, {light: {step1: string; step3: string; step6: string; step11: string; step12: string}; dark: {step1: string; step3: string; step6: string; step11: string; step12: string}}> = {
73+
gray: {light: {step1: '#fcfcfc', step3: '#f0f0f0', step6: '#d9d9d9', step11: '#646464', step12: '#202020'}, dark: {step1: '#111111', step3: '#222222', step6: '#3a3a3a', step11: '#b4b4b4', step12: '#eeeeee'}},
74+
mauve: {light: {step1: '#fdfcfd', step3: '#f1eef1', step6: '#d9d5dc', step11: '#645f6b', step12: '#211f26'}, dark: {step1: '#121113', step3: '#232225', step6: '#3c393f', step11: '#b5b2bc', step12: '#eeeef0'}},
75+
slate: {light: {step1: '#fcfcfd', step3: '#eef0f3', step6: '#d3d8e0', step11: '#5b6373', step12: '#1a2030'}, dark: {step1: '#111113', step3: '#212225', step6: '#363a3f', step11: '#b0b4ba', step12: '#edeef0'}},
76+
sage: {light: {step1: '#fbfdfc', step3: '#eff1f0', step6: '#d4dad7', step11: '#5f6563', step12: '#1a211e'}, dark: {step1: '#101211', step3: '#202221', step6: '#373b39', step11: '#adb5b2', step12: '#eceeed'}},
77+
olive: {light: {step1: '#fcfdfc', step3: '#eff1ef', step6: '#d5d8d3', step11: '#60655f', step12: '#1d211c'}, dark: {step1: '#111210', step3: '#212220', step6: '#383a36', step11: '#afb5ad', step12: '#eceeec'}},
78+
sand: {light: {step1: '#fdfdfc', step3: '#f1f0ef', step6: '#d7d5d0', step11: '#63635e', step12: '#21201c'}, dark: {step1: '#111110', step3: '#222221', step6: '#3b3a37', step11: '#b5b3ad', step12: '#eeeeec'}},
8279
};
8380

8481
const GRAY_NAMES = Object.keys(GRAY_COLORS);
@@ -106,30 +103,32 @@ function generateCSS(settings: ThemeSettings): string {
106103
const gray = GRAY_COLORS[settings.grayColor];
107104
if (!accent || !gray) return '';
108105

109-
const fg = accent.darkContrast ? '0 0% 0%' : '0 0% 100%';
106+
const fgLight = accent.darkContrast ? '0 0% 0%' : '0 0% 100%';
107+
const fgDark = '0 0% 0%'; // dark mode always uses dark text on light accent
110108
const radius = RADIUS_MAP[settings.radius] ?? '0.5rem';
111109
const scale = parseInt(settings.scaling) / 100;
112110

113111
const lines = [
112+
'/* Light mode */',
114113
':root {',
115-
` --p-primary: ${hsl(accent.step9)};`,
116-
` --p-primary-hover: ${hsl(accent.step10)};`,
117-
` --p-primary-foreground: ${fg};`,
118-
` --p-ring: ${hsl(accent.step8)};`,
119-
` --p-accent: ${hsl(accent.step3)};`,
120-
` --p-accent-foreground: ${hsl(accent.step11)};`,
121-
` --p-foreground: ${hsl(gray.step12)};`,
122-
` --p-card-foreground: ${hsl(gray.step12)};`,
123-
` --p-popover-foreground: ${hsl(gray.step12)};`,
124-
` --p-secondary-foreground: ${hsl(gray.step12)};`,
125-
` --p-muted-foreground: ${hsl(gray.step11)};`,
126-
` --p-border: ${hsl(gray.step6)};`,
127-
` --p-input: ${hsl(gray.step6)};`,
128-
` --p-muted: ${hsl(gray.step3)};`,
129-
` --p-secondary: ${hsl(gray.step3)};`,
130-
` --p-background: ${hsl(gray.step1)};`,
131-
` --p-card: ${hsl(gray.step1)};`,
132-
` --p-popover: ${hsl(gray.step1)};`,
114+
` --p-primary: ${hsl(accent.light.step9)};`,
115+
` --p-primary-hover: ${hsl(accent.light.step10)};`,
116+
` --p-primary-foreground: ${fgLight};`,
117+
` --p-ring: ${hsl(accent.light.step8)};`,
118+
` --p-accent: ${hsl(accent.light.step3)};`,
119+
` --p-accent-foreground: ${hsl(accent.light.step11)};`,
120+
` --p-foreground: ${hsl(gray.light.step12)};`,
121+
` --p-card-foreground: ${hsl(gray.light.step12)};`,
122+
` --p-popover-foreground: ${hsl(gray.light.step12)};`,
123+
` --p-secondary-foreground: ${hsl(gray.light.step12)};`,
124+
` --p-muted-foreground: ${hsl(gray.light.step11)};`,
125+
` --p-border: ${hsl(gray.light.step6)};`,
126+
` --p-input: ${hsl(gray.light.step6)};`,
127+
` --p-muted: ${hsl(gray.light.step3)};`,
128+
` --p-secondary: ${hsl(gray.light.step3)};`,
129+
` --p-background: ${hsl(gray.light.step1)};`,
130+
` --p-card: ${hsl(gray.light.step1)};`,
131+
` --p-popover: ${hsl(gray.light.step1)};`,
133132
` --p-radius: ${radius};`,
134133
];
135134

@@ -138,6 +137,59 @@ function generateCSS(settings: ThemeSettings): string {
138137
}
139138

140139
lines.push('}');
140+
lines.push('');
141+
142+
// Dark mode (automatic)
143+
lines.push('/* Dark mode (automatic) */');
144+
lines.push('@media (prefers-color-scheme: dark) {');
145+
lines.push(' :root {');
146+
lines.push(' color-scheme: dark;');
147+
lines.push(` --p-primary: ${hsl(accent.dark.step9)};`);
148+
lines.push(` --p-primary-hover: ${hsl(accent.dark.step10)};`);
149+
lines.push(` --p-primary-foreground: ${fgDark};`);
150+
lines.push(` --p-ring: ${hsl(accent.dark.step9)};`);
151+
lines.push(` --p-accent: ${hsl(accent.dark.step3)};`);
152+
lines.push(` --p-accent-foreground: ${hsl(accent.dark.step11)};`);
153+
lines.push(` --p-foreground: ${hsl(gray.dark.step12)};`);
154+
lines.push(` --p-card-foreground: ${hsl(gray.dark.step12)};`);
155+
lines.push(` --p-popover-foreground: ${hsl(gray.dark.step12)};`);
156+
lines.push(` --p-secondary-foreground: ${hsl(gray.dark.step12)};`);
157+
lines.push(` --p-muted-foreground: ${hsl(gray.dark.step11)};`);
158+
lines.push(` --p-border: ${hsl(gray.dark.step6)};`);
159+
lines.push(` --p-input: ${hsl(gray.dark.step6)};`);
160+
lines.push(` --p-muted: ${hsl(gray.dark.step3)};`);
161+
lines.push(` --p-secondary: ${hsl(gray.dark.step3)};`);
162+
lines.push(` --p-background: ${hsl(gray.dark.step1)};`);
163+
lines.push(` --p-card: ${hsl(gray.dark.step1)};`);
164+
lines.push(` --p-popover: ${hsl(gray.dark.step1)};`);
165+
lines.push(' }');
166+
lines.push('}');
167+
lines.push('');
168+
169+
// Dark mode (explicit class)
170+
lines.push('/* Dark mode (explicit) */');
171+
lines.push('.dark {');
172+
lines.push(' color-scheme: dark;');
173+
lines.push(` --p-primary: ${hsl(accent.dark.step9)};`);
174+
lines.push(` --p-primary-hover: ${hsl(accent.dark.step10)};`);
175+
lines.push(` --p-primary-foreground: ${fgDark};`);
176+
lines.push(` --p-ring: ${hsl(accent.dark.step9)};`);
177+
lines.push(` --p-accent: ${hsl(accent.dark.step3)};`);
178+
lines.push(` --p-accent-foreground: ${hsl(accent.dark.step11)};`);
179+
lines.push(` --p-foreground: ${hsl(gray.dark.step12)};`);
180+
lines.push(` --p-card-foreground: ${hsl(gray.dark.step12)};`);
181+
lines.push(` --p-popover-foreground: ${hsl(gray.dark.step12)};`);
182+
lines.push(` --p-secondary-foreground: ${hsl(gray.dark.step12)};`);
183+
lines.push(` --p-muted-foreground: ${hsl(gray.dark.step11)};`);
184+
lines.push(` --p-border: ${hsl(gray.dark.step6)};`);
185+
lines.push(` --p-input: ${hsl(gray.dark.step6)};`);
186+
lines.push(` --p-muted: ${hsl(gray.dark.step3)};`);
187+
lines.push(` --p-secondary: ${hsl(gray.dark.step3)};`);
188+
lines.push(` --p-background: ${hsl(gray.dark.step1)};`);
189+
lines.push(` --p-card: ${hsl(gray.dark.step1)};`);
190+
lines.push(` --p-popover: ${hsl(gray.dark.step1)};`);
191+
lines.push('}');
192+
141193
return lines.join('\n');
142194
}
143195

@@ -311,7 +363,7 @@ export function ThemeCustomizer() {
311363
{ACCENT_NAMES.map((name) => (
312364
<Swatch
313365
key={name}
314-
color={ACCENT_COLORS[name].step9}
366+
color={ACCENT_COLORS[name].light.step9}
315367
selected={settings.accentColor === name}
316368
onClick={() => setSettings((s) => ({...s, accentColor: name}))}
317369
label={name}
@@ -329,7 +381,7 @@ export function ThemeCustomizer() {
329381
{GRAY_NAMES.map((name) => (
330382
<Swatch
331383
key={name}
332-
color={GRAY_COLORS[name].step9 ?? GRAY_COLORS[name].step11}
384+
color={GRAY_COLORS[name].light.step11}
333385
selected={settings.grayColor === name}
334386
onClick={() => setSettings((s) => ({...s, grayColor: name}))}
335387
label={name}

0 commit comments

Comments
 (0)