Skip to content

Instantly share code, notes, and snippets.

@mike-at-redspace
Created September 8, 2025 10:17
Show Gist options
  • Save mike-at-redspace/0b9838d6681796c1965623992b1ddc9a to your computer and use it in GitHub Desktop.
Save mike-at-redspace/0b9838d6681796c1965623992b1ddc9a to your computer and use it in GitHub Desktop.
Color Palette Generator

Color Palette Generator

An interactive tool to generate beautiful color palettes from any base color. Pick a color or randomize one, then explore complementary, split, triadic, and other harmony schemes. Copy HEX codes or full CSS variables instantly.

A Pen by Alexandre Vacassin on CodePen.

License.

<div id="backgroundStrip" aria-hidden="true"></div>
<div id="backgroundGrid" aria-hidden="true"></div>
<div class="overlay" role="region" aria-label="Contrôles de génération de couleurs">
<div class="title">Color Palette Generator </div>
<div class="subtitle">This tool generates beautiful color palettes from any base color. Use the Random button or pick a color with the selector, then explore different harmony schemes. <a id="link" href="https://en.wikipedia.org/wiki/Color_scheme" target="_blank" rel="noopener">Learn More</a></div>
<div class="row" style="align-items:center">
<div class="input-wrap">
<input id="baseColor" type="text" value="#007bff" data-coloris aria-label="Couleur de base" />
</div>
<button class="btn" id="randomize">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 4l3 3l-3 3" />
<path d="M18 20l3 -3l-3 -3" />
<path d="M3 7h3a5 5 0 0 1 5 5a5 5 0 0 0 5 5h5" />
<path d="M21 7h-5a4.978 4.978 0 0 0 -3 1m-4 8a4.984 4.984 0 0 1 -3 1h-3" />
</svg>
</button>
<div style="margin-left:auto" class="controls-right">
<button class="btn" id="copyCss">Copy CSS</button>
<button class="btn" id="copyHexes">Copy HEX</button>
</div>
</div>
<div class="row">
<div class="tabs" id="tabs">
<button class="tab" data-filter="complementary">Complementary</button>
<button class="tab active" data-filter="split">Split complementary</button>
<button class="tab" data-filter="monochrome">Monochrome</button>
<button class="tab" data-filter="analogous">Analogous</button>
<button class="tab" data-filter="triadic">Triadic</button>
<button class="tab" data-filter="quadratic">Quadratic</button>
</div>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
const clamp = (v, a, b) => Math.min(Math.max(v, a), b);
const mod = (n, m) => ((n % m) + m) % m;
function parseColor(input) {
const s = String(input || "").trim();
let m = s.match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (m) {
let h = m[1];
if (h.length === 3)
h = h
.split("")
.map((c) => c + c)
.join("");
const num = parseInt(h, 16);
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
}
m = s.match(/^rgba?\(([^)]+)\)$/i);
if (m) {
const parts = m[1]
.split(/[,/ ]+/)
.map((p) => p.trim())
.filter(Boolean);
const [r, g, b] = parts;
const px = (v) =>
v.endsWith("%")
? Math.round(parseFloat(v) * 2.55)
: Math.round(parseFloat(v));
return { r: px(r), g: px(g), b: px(b) };
}
m = s.match(/^hsla?\(([^)]+)\)$/i);
if (m) {
const parts = m[1]
.split(/[,/ ]+/)
.map((p) => p.trim())
.filter(Boolean);
const [h, sPct, lPct] = parts;
const hN = parseFloat(h);
const sN = parseFloat(sPct) / (sPct.endsWith("%") ? 100 : 1);
const lN = parseFloat(lPct) / (lPct.endsWith("%") ? 100 : 1);
return hslToRgb(hN, sN, lN);
}
return { r: 26, g: 164, b: 255 };
}
function rgbToHex({ r, g, b }) {
const toHex = (v) => Math.round(v).toString(16).padStart(2, "0");
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
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,
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;
case b:
h = (r - g) / d + 4;
break;
}
h *= 60;
}
return { h, s, l };
}
function hslToRgb(h, s, l) {
h = mod(h, 360) / 360;
let r, g, b;
if (s === 0) r = g = b = l;
else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function hslToHex(h, s, l) {
return rgbToHex(hslToRgb(h, s, l));
}
function adjust(h, s, l, { dh = 0, ds = 0, dl = 0 } = {}) {
return {
h: mod(h + dh, 360),
s: clamp(s + ds, 0, 1),
l: clamp(l + dl, 0, 1)
};
}
function idealTextColor(hex) {
const { r, g, b } = parseColor(hex);
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "#0f172a" : "#FFFFFF";
}
function generateSchemes(baseHex) {
const baseRgb = parseColor(baseHex);
const { h, s, l } = rgbToHsl(baseRgb);
const complementary = [adjust(h, s, l, { dh: 180 })];
const split = [
adjust(h, s, l, { dh: 180 - 30 }),
adjust(h, s, l, { dh: 180 + 30 })
];
const analogous = [-60, -30, 30, 60].map((dh) => adjust(h, s, l, { dh }));
const triadic = [-120, 120].map((dh) => adjust(h, s, l, { dh }));
const quadratic = [90, 180, 270].map((dh) => adjust(h, s, l, { dh }));
const monoSteps = [-0.22, -0.12, 0.12, 0.22];
const monochrome = monoSteps.map((dl) =>
adjust(h, s, l, { dl, ds: dl > 0 ? -0.05 : 0.05 })
);
const toObj = (obj, i, cat) => {
const hex = hslToHex(obj.h, obj.s, obj.l);
return { hex, hsl: obj, category: cat, varName: `--color-${cat}-${i + 1}` };
};
const pack = (arr, cat) => arr.map((o, i) => toObj(o, i, cat));
return {
base: { hex: hslToHex(h, s, l), h, s, l },
complementary: pack(complementary, "complementary"),
split: pack(split, "split"),
monochrome: pack(monochrome, "monochrome"),
analogous: pack(analogous, "analogous"),
triadic: pack(triadic, "triadic"),
quadratic: pack(quadratic, "quadratic")
};
}
function uniqueAll(schemes, limit = 12) {
const all = [
...schemes.complementary,
...schemes.split,
...schemes.analogous,
...schemes.triadic,
...schemes.quadratic,
...schemes.monochrome
];
const seen = new Set();
const out = [];
for (const c of all) {
const key = c.hex.toUpperCase();
if (!seen.has(key)) {
seen.add(key);
out.push(c);
}
if (out.length >= limit) break;
}
return out;
}
function applyCssVars(colors, baseHex) {
const root = document.documentElement;
for (let i = 0; i < colors.length; i++) {
root.style.setProperty(
`--color-${String(i + 1).padStart(2, "0")}`,
colors[i]
);
}
root.style.setProperty("--color-base", baseHex);
}
const grid = document.getElementById("backgroundGrid");
const input = document.getElementById("baseColor");
const randomize = document.getElementById("randomize");
const tabs = document.getElementById("tabs");
const toast = document.getElementById("toast");
let currentFilter = "split";
let currentSchemes = null;
function showToast(msg) {
toast.textContent = msg;
toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 1400);
}
function layoutGrid(n) {
const cols = Math.ceil(Math.sqrt(n));
const rows = Math.ceil(n / cols);
grid.style.gridTemplateColumns = `repeat(${n}, 1fr)`;
grid.style.gridTemplateRows = `1fr`;
}
function render() {
const baseHex = (function () {
try {
return String(input.value).trim() || "#5eb0e5";
} catch (e) {
return "#5eb0e5";
}
})();
currentSchemes = generateSchemes(baseHex);
let items = [];
if (currentFilter === "all") {
const uniq = uniqueAll(currentSchemes, 12);
items = [
{
hex: currentSchemes.base.hex,
category: "base",
varName: "--color-base"
},
...uniq.map((c, i) => ({
hex: c.hex,
category: c.category,
varName: `--color-${String(i + 1).padStart(2, "0")}`
}))
];
} else {
const arr = currentSchemes[currentFilter] || [];
items = [
{
hex: currentSchemes.base.hex,
category: "base",
varName: "--color-base"
},
...arr.map((c, i) => ({
hex: c.hex,
category: c.category,
varName: `--color-${String(i + 1).padStart(2, "0")}`
}))
];
}
if (items.length > 12) items = items.slice(0, 12);
applyCssVars(
items.map((i) => i.hex),
currentSchemes.base.hex
);
layoutGrid(items.length);
grid.innerHTML = "";
items.forEach((it, idx) => {
const cell = document.createElement("div");
cell.className = "bg-cell";
cell.style.background = it.hex;
const textColor = idealTextColor(it.hex);
cell.style.color = textColor;
const code = document.createElement("div");
code.className = "code";
code.textContent = it.hex;
code.style.color = textColor;
const varEl = document.createElement("div");
varEl.className = "var";
varEl.textContent = it.varName;
varEl.style.color = textColor;
const copyBtn = document.createElement("button");
copyBtn.className = "copy";
copyBtn.textContent = "Copy";
copyBtn.onclick = () => {
navigator.clipboard
.writeText(it.hex)
.then(() => showToast(it.hex + " Copied ✔"));
};
copyBtn.style.background =
textColor === "#FFFFFF" ? "rgba(0,0,0,0.22)" : "rgba(255,255,255,0.4)";
copyBtn.style.color = textColor === "#FFFFFF" ? "#fff" : "#0f172a";
cell.appendChild(code);
cell.appendChild(varEl);
cell.appendChild(copyBtn);
grid.appendChild(cell);
const baseHex = currentSchemes ? currentSchemes.base.hex : "#1aa4ff";
tabs.querySelectorAll(".tab").forEach((tab) => {
if (tab.classList.contains("active")) {
tab.style.background = baseHex;
tab.style.color = idealTextColor(baseHex);
} else {
tab.style.background = "rgba(0, 0, 0, 0.1)";
tab.style.color = "#191919";
}
});
});
const strip = document.getElementById("backgroundStrip");
strip.innerHTML = "";
if (grid.children.length > 0) {
const firstColor = grid.firstElementChild.style.background;
const lastColor = grid.lastElementChild.style.background;
[firstColor, lastColor].forEach((color) => {
const cell = document.createElement("div");
cell.className = "bg-cell";
cell.style.background = color;
strip.appendChild(cell);
});
}
}
input.addEventListener("input", () => render());
randomize.addEventListener("click", () => {
const newHex =
"#" +
Math.floor(Math.random() * 0xffffff)
.toString(16)
.padStart(6, "0");
input.value = newHex;
if (window.Coloris && Coloris.setInstance) {
Coloris.setInstance("#baseColor", { value: newHex });
}
const clrField = input.closest(".clr-field");
if (clrField) {
clrField.style.color = newHex;
}
render();
});
tabs.addEventListener("click", (e) => {
const b = e.target.closest("button.tab");
if (!b) return;
tabs.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
b.classList.add("active");
currentFilter = b.dataset.filter;
render();
});
document.getElementById("copyCss").addEventListener("click", () => {
if (!currentSchemes) return;
let items = [];
if (currentFilter === "all") {
const uniq = uniqueAll(currentSchemes, 12);
items = [currentSchemes.base.hex, ...uniq.map((c) => c.hex)];
} else {
const arr = currentSchemes[currentFilter] || [];
items = [currentSchemes.base.hex, ...arr.map((c) => c.hex)];
}
if (items.length > 12) items = items.slice(0, 12);
const lines = [":root {", ` --color-base: ${items[0]};`];
items
.slice(1)
.forEach((h, i) =>
lines.push(` --color-${String(i + 1).padStart(2, "0")}: ${h};`)
);
lines.push("}");
navigator.clipboard
.writeText(lines.join("\n"))
.then(() => showToast("CSS Variables Copied ✔"));
});
document.getElementById("copyHexes").addEventListener("click", () => {
if (!currentSchemes) return;
let items = [];
if (currentFilter === "all") {
const uniq = uniqueAll(currentSchemes, 12);
items = [currentSchemes.base.hex, ...uniq.map((c) => c.hex)];
} else {
const arr = currentSchemes[currentFilter] || [];
items = [currentSchemes.base.hex, ...arr.map((c) => c.hex)];
}
if (items.length > 12) items = items.slice(0, 12);
navigator.clipboard
.writeText(items.join(", "))
.then(() => showToast("HEX Values Copied ✔"));
});
try {
if (window.Coloris) {
Coloris({
el: "#baseColor",
themeMode: "dark",
format: "hex",
formatToggle: true,
swatches: ["#aee1cd", "#ffe681", "#5eb0e5", "#ee7762", "#ba0c2e"]
});
}
} catch (e) {}
render();
let demoInterval = null;
let demoActive = true;
function stopDemo() {
demoActive = false;
if (demoInterval) {
clearInterval(demoInterval);
demoInterval = null;
}
["click", "keydown", "touchstart", "pointerdown"].forEach((evt) =>
window.removeEventListener(evt, stopDemoHandler, { capture: true })
);
}
function stopDemoHandler(e) {
if (e && e.isTrusted === false) return;
stopDemo();
}
function startDemo(intervalMs = 2000, startDelay = 600) {
if (demoInterval) return;
const tabButtons = Array.from(tabs.querySelectorAll(".tab"));
if (tabButtons.length === 1) return;
let i = 1;
setTimeout(() => {
demoInterval = setInterval(() => {
if (!demoActive) return;
const b = tabButtons[i % tabButtons.length];
tabs
.querySelectorAll(".tab")
.forEach((t) => t.classList.remove("active"));
b.classList.add("active");
currentFilter = b.dataset.filter;
render();
i++;
}, intervalMs);
}, startDelay);
}
["click", "keydown", "touchstart", "pointerdown"].forEach((evt) =>
window.addEventListener(evt, stopDemoHandler, { capture: true })
);
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
setTimeout(() => startDemo(), 80);
} else {
document.addEventListener("DOMContentLoaded", () =>
setTimeout(() => startDemo(), 80)
);
}
<script src="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js"></script>
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
body {
height: 100%;
margin: 0;
}
body,
button,
input {
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
Roboto, "Helvetica Neue", Arial;
}
#link {
color: #475569;
}
#backgroundStrip {
position: fixed;
inset: 0;
display: grid;
grid-template-columns: 1fr 1fr;
z-index: -1; /* ensure it’s behind everything */
}
#backgroundGrid {
position: fixed;
inset: 0;
display: grid;
grid-auto-rows: 1fr;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: 0;
width: 100vw;
height: 200vh;
margin-top: -50vh;
z-index: 0;
}
#backgroundGrid .bg-cell {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 18px;
box-sizing: border-box;
gap: 10px;
transform: rotate(18deg);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.bg-cell .code {
font-weight: 700;
letter-spacing: 0.01em;
font-size: 2rem;
transform: rotate(-18deg);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.bg-cell .var {
font-size: 12px;
opacity: 0.9;
padding: 4px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
display: none;
transform: rotate(-18deg);
}
.bg-cell button.copy {
margin-top: 8px;
padding: 8px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 700;
transform: rotate(-18deg);
margin-left: 6%;
mix-blend-mode: hard-light;
}
#randomize {
padding: 16px 16px;
}
.overlay {
font-family: "Inter";
position: fixed;
left: 50%;
top: 0;
transform: translate(-50%, 0%);
z-index: 20;
background: rgba(255, 255, 255, 0.88);
border-radius: 0px 0px 16px 16px;
padding: 24px 24px 8px 24px;
box-shadow: 0 12px 40px rgba(2, 6, 23, 0.28);
backdrop-filter: blur(6px) saturate(120%);
max-width: 720px;
}
.clr-field {
flex: 1;
font-weight: 900;
}
.clr-field button {
border-radius: 4px;
overflow: hidden;
cursor: pointer;
}
.clr-alpha {
display: none;
}
.title {
font-size: 1.5em;
font-weight: 800;
margin-bottom: 0.5em;
}
.subtitle {
font-size: 13px;
color: #475569;
margin-bottom: 14px;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.input-wrap {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
background: #fff;
border: 1px solid rgb(230, 230, 230);
height: 32px !important;
border-radius: 8px;
}
.input-wrap input {
border: none;
background: transparent;
outline: none;
font-size: 16px;
padding: 6px;
}
.btn {
padding: 16px 24px;
border-radius: 8px;
border: 1px solid rgb(230, 230, 230);
background: white;
color: #191919;
cursor: pointer;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.btn.ghost {
background: transparent;
}
.tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tab {
padding: 8px 14px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 400;
font-size: 14px;
}
.tab.active {
background: #191919;
color: white;
}
.controls-right {
display: flex;
gap: 8px;
margin-left: auto;
}
.toast {
position: fixed;
right: 14px;
bottom: 14px;
background: #111827;
color: white;
padding: 10px 12px;
border-radius: 8px;
opacity: 0;
transform: translateY(8px);
transition: all 0.28s ease;
z-index: 60;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
@media only screen and (max-width: 1220px) {
.input-wrap {
width: 70%;
}
.controls-right {
margin-left: 0 !important;
}
.code {
font-size: 1rem !important;
}
#backgroundGrid {
height: 100vh;
margin-top: 0vh;
}
#backgroundGrid .bg-cell,
.bg-cell .code,
.bg-cell button.copy {
transform: rotate(0deg);
}
.bg-cell button.copy {
margin-left: 0;
}
#backgroundGrid .bg-cell {
justify-content: flex-end;
}
}
@media only screen and (max-width: 600px) {
.bg-cell .code {
font-size: 0.7rem !important;
}
.bg-cell button.copy {
font-size: 0.7rem;
padding: 4px 8px;
}
#backgroundGrid .bg-cell {
padding: 16px 0px;
}
}
<link href="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment