color-mix() Theme Generator
Pick one base hue and watch an entire dark-UI palette derive itself live from CSS color-mix() in oklch — tints, shades, hover, active and border tokens — previewed on a real sample interface with copy-ready CSS and a relative-color-syntax variant.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
/* ---- Themed tokens (overwritten live via JS on #scope) ---- */
--base: #8b5cf6;
--mix-space: oklch;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
background:
radial-gradient(1100px 600px at 80% -10%, rgba(139, 92, 246, 0.12), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(34, 211, 238, 0.08), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
min-height: 100vh;
}
.mono,
code,
pre {
font-family: "JetBrains Mono", ui-monospace, monospace;
}
.app {
max-width: 1120px;
margin: 0 auto;
padding: clamp(20px, 4vw, 40px) clamp(16px, 4vw, 32px) 56px;
}
/* ---------- Header ---------- */
.app__head {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 28px;
}
.app__title h1 {
margin: 0 0 6px;
font-size: clamp(1.4rem, 3.4vw, 1.9rem);
font-weight: 700;
letter-spacing: -0.02em;
}
.app__title p {
margin: 0;
color: var(--muted);
font-size: 0.94rem;
max-width: 46ch;
}
.app__title code {
color: var(--accent-2);
font-size: 0.86em;
background: rgba(255, 255, 255, 0.05);
padding: 1px 6px;
border-radius: 6px;
}
.mode {
display: flex;
align-items: center;
gap: 10px;
}
.mode__label {
color: var(--muted);
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.seg {
display: inline-flex;
padding: 4px;
gap: 4px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.seg__btn {
appearance: none;
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 0.85rem;
font-weight: 500;
padding: 7px 13px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.seg__btn:hover {
color: var(--ink);
}
.seg__btn.is-active {
background: var(--surface-2);
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--line-2);
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 320px 1fr;
gap: 18px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
}
/* ---------- Controls ---------- */
.controls {
display: flex;
flex-direction: column;
gap: 20px;
position: sticky;
top: 20px;
}
.field {
display: flex;
flex-direction: column;
gap: 9px;
}
.field__row {
display: flex;
align-items: center;
justify-content: space-between;
}
.field label,
.field__label {
font-size: 0.86rem;
font-weight: 500;
color: var(--ink);
}
.field output.mono {
font-size: 0.8rem;
color: var(--muted);
}
.picker {
position: relative;
display: flex;
align-items: center;
gap: 12px;
}
input[type="color"] {
--sz: 46px;
width: var(--sz);
height: var(--sz);
padding: 0;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: transparent;
cursor: pointer;
flex: 0 0 auto;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 3px;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 8px;
}
input[type="color"]::-moz-color-swatch {
border: none;
border-radius: 8px;
}
.swatch-preview {
flex: 1;
height: 46px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: linear-gradient(
120deg,
color-mix(in oklch, var(--base) 82%, white) 0%,
var(--base) 50%,
color-mix(in oklch, var(--base) 78%, black) 100%
);
}
/* Range inputs */
input[type="range"] {
--track: var(--surface-2);
appearance: none;
width: 100%;
height: 6px;
border-radius: 999px;
background: var(--track);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--base);
border: 2px solid #fff;
box-shadow: 0 0 0 1px var(--line-2), 0 2px 6px rgba(0, 0, 0, 0.5);
transition: transform 0.12s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.12);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--base);
border: 2px solid #fff;
box-shadow: 0 0 0 1px var(--line-2);
}
input[type="range"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in oklch, var(--base) 45%, transparent);
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.preset {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid var(--line-2);
background: var(--p);
cursor: pointer;
padding: 0;
transition: transform 0.14s ease, box-shadow 0.14s ease;
}
.preset:hover {
transform: translateY(-2px) scale(1.06);
}
.preset.is-on {
box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--p);
}
.preset:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px #fff;
}
.support {
margin: 0;
font-size: 0.76rem;
color: var(--muted);
padding: 9px 11px;
border-radius: var(--r-sm);
background: var(--surface-2);
border: 1px solid var(--line);
line-height: 1.45;
}
.support.warn {
color: var(--warn);
border-color: color-mix(in oklch, var(--warn) 40%, transparent);
}
/* ---------- Preview scope: themed tokens ---------- */
.preview__scope {
--tint-strong: var(--base);
--tint: var(--base);
--soft: var(--base);
--hover: var(--base);
--active: var(--base);
--shade: var(--base);
--border: var(--base);
--ring: var(--base);
--on-base: #fff;
display: flex;
flex-direction: column;
gap: 18px;
}
.ramp {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
}
.ramp__cell {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.ramp__cell i {
display: block;
height: 52px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
}
.ramp__cell b {
font: 500 0.64rem/1.2 "JetBrains Mono", monospace;
color: var(--muted);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ramp__cell[data-tok="tint-strong"] i { background: var(--tint-strong); }
.ramp__cell[data-tok="tint"] i { background: var(--tint); }
.ramp__cell[data-tok="soft"] i { background: var(--soft); }
.ramp__cell[data-tok="base"] i { background: var(--base); }
.ramp__cell[data-tok="hover"] i { background: var(--hover); }
.ramp__cell[data-tok="active"] i { background: var(--active); }
.ramp__cell[data-tok="shade"] i { background: var(--shade); }
/* Demo card */
.demo-card {
background: var(--soft);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 22px;
transition: background 0.25s ease, border-color 0.25s ease;
}
.demo-card__top {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.badge {
font-size: 0.72rem;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
color: var(--on-base);
background: var(--base);
}
.demo-card__meta {
font-size: 0.78rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.demo-card h3 {
margin: 0 0 6px;
font-size: 1.2rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.demo-card__body {
margin: 0 0 16px;
color: var(--muted);
font-size: 0.9rem;
max-width: 52ch;
}
.progress {
height: 8px;
border-radius: 999px;
background: var(--tint-strong);
overflow: hidden;
margin-bottom: 18px;
}
.progress__bar {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--base), var(--hover));
transition: width 0.4s ease;
}
.btns {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
}
.btn {
appearance: none;
font: inherit;
font-size: 0.88rem;
font-weight: 600;
padding: 10px 16px;
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease, transform 0.1s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--solid {
background: var(--base);
color: var(--on-base);
border: 1px solid var(--base);
}
.btn--solid:hover {
background: var(--hover);
border-color: var(--hover);
}
.btn--solid:active {
background: var(--active);
border-color: var(--active);
}
.btn--soft {
background: var(--tint);
color: var(--base);
border: 1px solid var(--border);
}
.btn--soft:hover {
background: var(--tint-strong);
}
.btn--ghost {
background: transparent;
color: var(--base);
border: 1px solid var(--border);
}
.btn--ghost:hover {
background: var(--tint);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ring);
}
.field-demo {
display: flex;
flex-direction: column;
gap: 7px;
margin-bottom: 18px;
}
.field-demo label {
font-size: 0.82rem;
font-weight: 500;
color: var(--ink);
}
.field-demo input {
font: inherit;
font-size: 0.9rem;
padding: 10px 12px;
border-radius: var(--r-md);
background: var(--bg);
color: var(--ink);
border: 1px solid var(--border);
transition: box-shadow 0.16s ease, border-color 0.16s ease;
}
.field-demo input::placeholder {
color: var(--muted);
}
.field-demo input:focus {
outline: none;
border-color: var(--base);
box-shadow: 0 0 0 3px var(--ring);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font-size: 0.78rem;
font-weight: 500;
padding: 5px 12px;
border-radius: 999px;
color: var(--muted);
background: var(--surface-2);
border: 1px solid var(--line);
}
.chip.is-on {
color: var(--base);
background: var(--tint);
border-color: var(--border);
}
/* ---------- Code panel ---------- */
.code {
margin-top: 18px;
padding: 0;
overflow: hidden;
}
.code__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
}
.code__title {
font-size: 0.86rem;
font-weight: 600;
color: var(--ink);
}
.code__engine {
font: 500 0.74rem "JetBrains Mono", monospace;
color: var(--accent-2);
margin-left: 6px;
}
.copy {
appearance: none;
font: inherit;
font-size: 0.8rem;
font-weight: 500;
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 6px 14px;
cursor: pointer;
transition: background 0.16s ease;
}
.copy:hover {
background: color-mix(in oklch, var(--surface-2) 70%, var(--accent));
}
.copy.is-done {
color: var(--ok);
border-color: color-mix(in oklch, var(--ok) 40%, transparent);
}
.code__pre {
margin: 0;
padding: 18px;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.6;
color: #cfd2e6;
max-height: 340px;
}
.code__pre .tok-prop { color: var(--accent-2); }
.code__pre .tok-fn { color: var(--warn); }
.code__pre .tok-comment { color: var(--muted); font-style: italic; }
.code__pre .tok-val { color: var(--ok); }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.controls {
position: static;
}
}
@media (max-width: 520px) {
.app__head {
align-items: stretch;
}
.ramp {
grid-template-columns: repeat(4, 1fr);
}
.ramp__cell b {
font-size: 0.56rem;
}
.btns .btn {
flex: 1 1 auto;
text-align: center;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
}
}(() => {
"use strict";
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
// ---- Feature detection ---------------------------------------------------
const supportsMix = CSS.supports("color", "color-mix(in oklch, red, blue)");
const supportsOklch = CSS.supports("color", "oklch(0.7 0.1 200)");
const supportsRelative = CSS.supports("color", "oklch(from red l c h)");
// Fall back to sRGB interpolation when oklch mixing isn't available.
const mixSpace = supportsMix && supportsOklch ? "oklch" : "srgb";
// ---- Token recipes -------------------------------------------------------
// Each token = { mix: [colorA-percent], relative: {l,c,h adjust} }
// `p` is scaled by the intensity multiplier at build time.
const RECIPES = [
{ key: "tint-strong", label: "Tint strong", mixWith: "white", mixPct: 88, rel: { l: "* 1.9", c: "* 0.35" } },
{ key: "tint", label: "Tint", mixWith: "white", mixPct: 80, rel: { l: "* 1.7", c: "* 0.5" } },
{ key: "soft", label: "Soft surface", mixWith: "black", mixPct: 90, rel: { l: "* 0.5", c: "* 0.7" } },
{ key: "base", label: "Base", mixWith: null, mixPct: 0, rel: null },
{ key: "hover", label: "Hover", mixWith: "white", mixPct: 18, rel: { l: "+ 0.06", c: "* 1" } },
{ key: "active", label: "Active", mixWith: "black", mixPct: 22, rel: { l: "- 0.07", c: "* 1" } },
{ key: "shade", label: "Shade", mixWith: "black", mixPct: 45, rel: { l: "* 0.55", c: "* 0.9" } },
];
// ---- Elements ------------------------------------------------------------
const scope = $("#scope");
const baseColor = $("#baseColor");
const basePreview = $("#basePreview");
const baseHex = $("#baseHex");
const hue = $("#hue");
const hueVal = $("#hueVal");
const intensity = $("#intensity");
const intensityVal = $("#intensityVal");
const codeOut = $("#codeOut");
const codeEngine = $("#codeEngine");
const copyBtn = $("#copyBtn");
const supportEl = $("#support");
const engineSeg = $("#engineSeg");
let engine = "mix"; // default engine: color-mix()
let base = "#8b5cf6";
// ---- Color helpers -------------------------------------------------------
function hexToRgb(hex) {
const h = hex.replace("#", "");
const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
function rgbToHex({ r, g, b }) {
return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
}
function rgbToHsl({ r, g, b }) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
default: h = (r - g) / d + 4;
}
h *= 60;
}
return { h, s, l };
}
function hslToRgb({ h, s, l }) {
h /= 360;
const f = (n) => {
const k = (n + h * 12) % 12;
const a = s * Math.min(l, 1 - l);
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
};
return { r: f(0) * 255, g: f(8) * 255, b: f(4) * 255 };
}
// Relative luminance for choosing readable text on the base color.
function readableText(hex) {
const { r, g, b } = hexToRgb(hex);
const lin = (c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
return L > 0.45 ? "#0c0d10" : "#ffffff";
}
// ---- CSS value builders --------------------------------------------------
// Intensity multiplier: 100 => 1.0
function scaledPct(pct, mult) {
return Math.max(0, Math.min(100, Math.round(pct * mult)));
}
function mixValue(recipe, mult) {
if (!recipe.mixWith) return "var(--base)";
const pct = scaledPct(recipe.mixPct, recipe.mixWith === "white" ? mult : mult);
// color-mix(in <space>, var(--base) X%, white)
const basePct = 100 - pct;
return `color-mix(in ${mixSpace}, var(--base) ${basePct}%, ${recipe.mixWith})`;
}
function relativeValue(recipe) {
if (!recipe.rel) return "var(--base)";
const { l, c } = recipe.rel;
return `oklch(from var(--base) calc(l ${l}) calc(c ${c}) h)`;
}
function tokenValue(recipe, mult) {
if (engine === "relative" && supportsRelative) {
return relativeValue(recipe);
}
return mixValue(recipe, mult);
}
// ---- Apply ---------------------------------------------------------------
function apply() {
const mult = parseInt(intensity.value, 10) / 100;
scope.style.setProperty("--base", base);
scope.style.setProperty("--on-base", readableText(base));
RECIPES.forEach((r) => {
if (r.key === "base") {
scope.style.setProperty("--base", base);
return;
}
scope.style.setProperty("--" + r.key, tokenValue(r, mult));
});
// Border + ring derived from tint / base
scope.style.setProperty(
"--border",
`color-mix(in ${mixSpace}, var(--base) 55%, transparent)`
);
scope.style.setProperty(
"--ring",
`color-mix(in ${mixSpace}, var(--base) 40%, transparent)`
);
// root swatch preview uses --base too
document.documentElement.style.setProperty("--base", base);
renderCode(mult);
}
// ---- Generated code panel ------------------------------------------------
function esc(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function highlight(line) {
let s = esc(line);
s = s.replace(/(\/\*.*?\*\/)/g, '<span class="tok-comment">$1</span>');
s = s.replace(/(--[\w-]+)(?=\s*:)/g, '<span class="tok-prop">$1</span>');
s = s.replace(/\b(color-mix|oklch|calc)\b/g, '<span class="tok-fn">$1</span>');
s = s.replace(/(#[0-9a-fA-F]{3,8})/g, '<span class="tok-val">$1</span>');
return s;
}
function renderCode(mult) {
const useRel = engine === "relative" && supportsRelative;
const lines = [];
lines.push(":root {");
lines.push(` --base: ${base};`);
if (!useRel) lines.push(` /* palette mixed in ${mixSpace} */`);
else lines.push(` /* relative color syntax in oklch */`);
RECIPES.forEach((r) => {
if (r.key === "base") return;
const val = useRel ? relativeValue(r) : mixValue(r, mult);
lines.push(` --${r.key}: ${val};`);
});
lines.push(` --border: color-mix(in ${mixSpace}, var(--base) 55%, transparent);`);
lines.push(` --ring: color-mix(in ${mixSpace}, var(--base) 40%, transparent);`);
lines.push("}");
codeOut.innerHTML = lines.map(highlight).join("\n");
codeOut.dataset.raw = lines.join("\n");
codeEngine.textContent = useRel ? "oklch(from …)" : "color-mix()";
}
// ---- Sync base <-> hue ---------------------------------------------------
function setBase(hex, { syncHue = true, syncPicker = true } = {}) {
base = hex.toLowerCase();
baseHex.textContent = base;
if (syncPicker) baseColor.value = base;
if (syncHue) {
const { h } = rgbToHsl(hexToRgb(base));
hue.value = Math.round(h);
hueVal.innerHTML = Math.round(h) + "°";
}
updatePresetActive();
apply();
}
function setHue(deg) {
hueVal.innerHTML = deg + "°";
const { s, l } = rgbToHsl(hexToRgb(base));
// keep saturation/lightness, rotate hue; guard low-sat greys
const sat = s < 0.12 ? 0.6 : s;
const lig = l < 0.15 || l > 0.85 ? 0.6 : l;
const hex = rgbToHex(hslToRgb({ h: deg, s: sat, l: lig }));
setBase(hex, { syncHue: false });
}
function updatePresetActive() {
$$(".preset").forEach((p) => {
p.classList.toggle("is-on", p.dataset.color.toLowerCase() === base);
p.setAttribute("aria-pressed", p.classList.contains("is-on") ? "true" : "false");
});
}
// ---- Wire events ---------------------------------------------------------
baseColor.addEventListener("input", (e) => setBase(e.target.value, { syncPicker: false }));
hue.addEventListener("input", (e) => setHue(parseInt(e.target.value, 10)));
intensity.addEventListener("input", (e) => {
intensityVal.textContent = e.target.value + "%";
apply();
});
$$(".preset").forEach((p) => {
p.addEventListener("click", () => setBase(p.dataset.color));
});
engineSeg.addEventListener("click", (e) => {
const btn = e.target.closest(".seg__btn");
if (!btn) return;
engine = btn.dataset.engine;
$$(".seg__btn", engineSeg).forEach((b) => {
const on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
if (engine === "relative" && !supportsRelative) {
supportEl.textContent =
"Relative color syntax (oklch(from …)) isn't supported by this browser — the preview keeps mixing, but the generated CSS shows the modern form.";
supportEl.classList.add("warn");
} else {
updateSupportNote();
}
apply();
});
copyBtn.addEventListener("click", async () => {
const text = codeOut.dataset.raw || codeOut.textContent;
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
copyBtn.textContent = "Copied!";
copyBtn.classList.add("is-done");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("is-done");
}, 1400);
});
function updateSupportNote() {
supportEl.classList.remove("warn");
if (!supportsMix) {
supportEl.textContent =
"Your browser lacks color-mix() — the palette falls back to static values. Update to a 2023+ browser for the full effect.";
supportEl.classList.add("warn");
} else if (mixSpace === "srgb") {
supportEl.textContent =
"color-mix() is supported, but oklch interpolation is not — falling back to gentler sRGB mixing.";
supportEl.classList.add("warn");
} else {
supportEl.innerHTML =
"Live palette derived in <b>oklch</b> via color-mix()" +
(supportsRelative ? " — relative color syntax also available." : ".");
}
}
// ---- Init ----------------------------------------------------------------
updateSupportNote();
setBase(base);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>color-mix() Theme Generator</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" id="app">
<header class="app__head">
<div class="app__title">
<h1>color-mix() Theme Generator</h1>
<p>
One base hue → a whole palette, derived live in CSS with
<code>color-mix()</code> in <code>oklch</code>.
</p>
</div>
<div class="mode" role="group" aria-label="Generation engine">
<span class="mode__label">Engine</span>
<div class="seg" id="engineSeg">
<button type="button" class="seg__btn is-active" data-engine="mix" aria-pressed="true">
color-mix()
</button>
<button type="button" class="seg__btn" data-engine="relative" aria-pressed="false">
relative color
</button>
</div>
</div>
</header>
<div class="layout">
<!-- Controls -->
<section class="panel controls" aria-label="Theme controls">
<div class="field">
<div class="field__row">
<label for="baseColor">Base color</label>
<output id="baseHex" class="mono">#8b5cf6</output>
</div>
<div class="picker">
<input type="color" id="baseColor" value="#8b5cf6" aria-label="Base color picker" />
<div class="swatch-preview" id="basePreview" aria-hidden="true"></div>
</div>
</div>
<div class="field">
<div class="field__row">
<label for="hue">Hue</label>
<output id="hueVal" class="mono">262°</output>
</div>
<input type="range" id="hue" min="0" max="360" step="1" value="262" aria-describedby="hueVal" />
</div>
<div class="field">
<div class="field__row">
<label for="intensity">Tint / shade intensity</label>
<output id="intensityVal" class="mono">100%</output>
</div>
<input type="range" id="intensity" min="40" max="140" step="1" value="100" aria-describedby="intensityVal" />
</div>
<div class="field">
<span class="field__label">Presets</span>
<div class="presets" id="presets" role="group" aria-label="Color presets">
<button type="button" class="preset" data-color="#8b5cf6" style="--p:#8b5cf6" aria-label="Violet"></button>
<button type="button" class="preset" data-color="#22d3ee" style="--p:#22d3ee" aria-label="Cyan"></button>
<button type="button" class="preset" data-color="#34d399" style="--p:#34d399" aria-label="Emerald"></button>
<button type="button" class="preset" data-color="#fbbf24" style="--p:#fbbf24" aria-label="Amber"></button>
<button type="button" class="preset" data-color="#f87171" style="--p:#f87171" aria-label="Rose"></button>
<button type="button" class="preset" data-color="#f472b6" style="--p:#f472b6" aria-label="Pink"></button>
<button type="button" class="preset" data-color="#60a5fa" style="--p:#60a5fa" aria-label="Blue"></button>
</div>
</div>
<p class="support" id="support" aria-live="polite"></p>
</section>
<!-- Preview -->
<section class="panel preview" id="preview" aria-label="Themed UI preview">
<div class="preview__scope" id="scope">
<div class="ramp" id="ramp" aria-label="Generated color ramp">
<span class="ramp__cell" data-tok="tint-strong"><i></i><b>tint-strong</b></span>
<span class="ramp__cell" data-tok="tint"><i></i><b>tint</b></span>
<span class="ramp__cell" data-tok="soft"><i></i><b>soft</b></span>
<span class="ramp__cell" data-tok="base"><i></i><b>base</b></span>
<span class="ramp__cell" data-tok="hover"><i></i><b>hover</b></span>
<span class="ramp__cell" data-tok="active"><i></i><b>active</b></span>
<span class="ramp__cell" data-tok="shade"><i></i><b>shade</b></span>
</div>
<div class="demo-card">
<div class="demo-card__top">
<span class="badge">New</span>
<span class="demo-card__meta">Themed component</span>
</div>
<h3>Deploy pipeline</h3>
<p class="demo-card__body">
Every accent below is mixed from your single base color — no extra variables authored by hand.
</p>
<div class="progress" role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Build progress">
<span class="progress__bar" style="width:68%"></span>
</div>
<div class="btns">
<button type="button" class="btn btn--solid">Primary action</button>
<button type="button" class="btn btn--soft">Secondary</button>
<button type="button" class="btn btn--ghost">Ghost</button>
</div>
<div class="field-demo">
<label for="demoInput">Focusable input</label>
<input type="text" id="demoInput" placeholder="Tab here to see the focus ring" />
</div>
<div class="chips">
<span class="chip is-on">active</span>
<span class="chip">draft</span>
<span class="chip">queued</span>
</div>
</div>
</div>
</section>
</div>
<!-- Generated CSS -->
<section class="panel code" aria-label="Generated CSS">
<div class="code__head">
<span class="code__title">Generated CSS <span class="code__engine" id="codeEngine">color-mix()</span></span>
<button type="button" class="copy" id="copyBtn">Copy</button>
</div>
<pre class="code__pre"><code id="codeOut"></code></pre>
</section>
</main>
<script src="script.js"></script>
</body>
</html>A live theming playground built almost entirely in CSS. You choose a single base color, and every other palette token — subtle surface tints, deep shades, hover and active states, borders and focus rings — is computed on the fly with color-mix() interpolated in the oklch color space. Because the mixing happens in CSS from one custom property, the whole sample UI (buttons, cards, badges, an input, a progress bar) re-themes instantly the moment the base hue changes, with no per-token JavaScript math.
Interact by dragging the native color picker, nudging the hue with the wheel or arrow keys, tweaking the tint/shade intensity slider, or clicking a preset swatch. A toggle flips the whole engine between color-mix() and the newer relative color syntax (oklch(from …)), and the generated CSS panel updates so you can copy the exact tokens into your own stylesheet.
The demo showcases the CSS color-mix() function and relative color syntax in oklch, plus @supports feature detection that gracefully falls back to srgb mixing when a browser lacks oklch interpolation.