Yo folks, I’m cleaning up our component styling and trying to stop hardcoding random hex colors all over the place. I wanted one source of truth that spits out a palette + gradients, but the failure mode is obvious: you can generate colors that look fine yet miss contrast once you swap themes.
// Generate a theme palette from a single hue using HSL
export function makeTheme({
hue = 210,
sat = 70,
mode = "light", // "light" | "dark"
} = {}) {
const isDark = mode === "dark";
const ramp = [
["--bg", isDark ? 8 : 98],
["--surface", isDark ? 14 : 96],
["--text", isDark ? 92 : 12],
["--muted", isDark ? 70 : 42],
["--primary", isDark ? 62 : 46],
["--primary-2", isDark ? 54 : 56],
];
const vars = Object.fromEntries(
ramp.map(([name, l]) => [name, `hsl(${hue} ${sat}% ${l}%)`])
);
vars["--gradient"] = `linear-gradient(135deg, ${vars["--primary"]}, ${vars["--primary-2"]})`;
// quick contrast hint (not WCAG-perfect): estimate luminance from HSL lightness
const approxLum = (hsl) => Number(hsl.match(/(\d+(?:\.\d+)?)%\)$/)?.[1] ?? 50) / 100;
vars["--contrast-hint"] =
Math.abs(approxLum(vars["--text"]) - approxLum(vars["--bg"])) > 0.75
? "ok"
: "risky";
return vars;
}
Neat part is it keeps components dumb: they just consume var(--primary) etc, and swapping themes is just swapping a hue/mode, plus you get a gradient for free.