A small HSL theme palette helper that outputs CSS variables

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.