Color Generator
Generate random colours, build palettes, convert between HEX and RGB, and copy colour codes to the clipboard.
Project Overview
Colours are everywhere in frontend work. This project practises bitwise arithmetic (HEX conversion), HSL colour math, clipboard API, and dynamic DOM — all in one visually satisfying tool.
What you will build
- Random colour generator — click to generate a new colour
- Large colour preview panel
- Display HEX, RGB, and HSL values simultaneously
- Copy any format to clipboard with one click
- Palette generator — produce 5 complementary shades
- Saved colours shelf (up to 12, persisted in localStorage)
Concepts used
- Bitwise operations (
>>and&) for colour channel extraction - HSL to RGB conversion algorithm
- Template literals for colour strings
navigator.clipboard.writeText()- Dynamic background colour updates
Key Logic
Random HEX generation
function randomHex() {
// Generate a random integer 0–16777215 (0xFFFFFF)
const n = Math.floor(Math.random() * 0xFFFFFF + 1);
// toString(16) converts to hex; padStart ensures 6 digits
return '#' + n.toString(16).padStart(6, '0').toUpperCase();
}
HEX to RGB
function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16); // remove # and parse
return {
r: (n >> 16) & 0xFF, // top 8 bits
g: (n >> 8) & 0xFF, // middle 8 bits
b: n & 0xFF // bottom 8 bits
};
}
RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0, s = 0;
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)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
Palette: shades of the same hue
function buildPalette(hex) {
const { r, g, b } = hexToRgb(hex);
const { h, s } = rgbToHsl(r, g, b);
// Five evenly spaced lightness values
return [20, 35, 50, 65, 80].map(l => hslToHex(h, s, l));
}
Complete Working Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Generator</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0;
min-height: 100vh; display: flex; align-items: center;
justify-content: center; padding: 1rem; }
.wrap { width: 100%; max-width: 480px; }
.preview {
height: 200px; border-radius: 1rem; margin-bottom: 1.25rem;
transition: background .3s; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: .9rem; color: #fff8; user-select: none;
box-shadow: 0 8px 32px #0004;
}
.codes {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: .75rem; margin-bottom: 1.25rem;
}
.code-box {
background: #1e293b; border-radius: .6rem; padding: .75rem;
text-align: center; cursor: pointer; transition: background .15s;
border: 2px solid transparent;
}
.code-box:hover { background: #334155; border-color: #3b82f6; }
.code-label { font-size: .7rem; color: #64748b; margin-bottom: .3rem; text-transform: uppercase; letter-spacing: .05em; }
.code-val { font-family: monospace; font-size: .9rem; color: #38bdf8; }
.row { display: flex; gap: .75rem; margin-bottom: 1.25rem; }
.btn {
flex: 1; padding: .8rem; background: #3b82f6; color: #fff;
border: none; border-radius: .6rem; font-size: .95rem;
font-weight: 600; cursor: pointer;
}
.btn:hover { background: #2563eb; }
.btn.secondary { background: #1e293b; color: #94a3b8; }
.btn.secondary:hover { background: #334155; }
h3 { font-size: .85rem; color: #64748b; text-transform: uppercase;
letter-spacing: .05em; margin-bottom: .6rem; }
.palette { display: flex; gap: .5rem; margin-bottom: 1.25rem; border-radius: .6rem; overflow: hidden; }
.swatch {
flex: 1; height: 50px; cursor: pointer; transition: transform .15s;
position: relative;
}
.swatch:hover { transform: scaleY(1.1); z-index: 1; }
.saved-shelf { display: flex; flex-wrap: wrap; gap: .5rem; }
.saved-swatch {
width: 40px; height: 40px; border-radius: .4rem;
cursor: pointer; border: 2px solid transparent;
transition: transform .15s, border-color .15s;
}
.saved-swatch:hover { transform: scale(1.15); border-color: #fff4; }
.toast {
position: fixed; bottom: 2rem; left: 50%;
transform: translateX(-50%) translateY(100px);
background: #22c55e; color: #fff;
padding: .5rem 1.5rem; border-radius: 999px;
font-size: .85rem; transition: transform .3s; pointer-events: none;
}
.toast.show { transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<div class="wrap">
<div class="preview" id="preview">Click to generate</div>
<div class="codes">
<div class="code-box" id="hex-box">
<div class="code-label">HEX</div>
<div class="code-val" id="hex-val">—</div>
</div>
<div class="code-box" id="rgb-box">
<div class="code-label">RGB</div>
<div class="code-val" id="rgb-val">—</div>
</div>
<div class="code-box" id="hsl-box">
<div class="code-label">HSL</div>
<div class="code-val" id="hsl-val">—</div>
</div>
</div>
<div class="row">
<button class="btn" id="gen-btn">Generate</button>
<button class="btn secondary" id="save-btn">Save Colour</button>
</div>
<h3>Palette</h3>
<div class="palette" id="palette"></div>
<h3>Saved</h3>
<div class="saved-shelf" id="saved-shelf"></div>
</div>
<div class="toast" id="toast"></div>
<script>
const STORAGE_KEY = 'saved_colors';
let current = '';
const preview = document.querySelector('#preview');
const hexVal = document.querySelector('#hex-val');
const rgbVal = document.querySelector('#rgb-val');
const hslVal = document.querySelector('#hsl-val');
const palette = document.querySelector('#palette');
const savedShelf = document.querySelector('#saved-shelf');
const toast = document.querySelector('#toast');
// ── Colour math ──────────────────────────────────────────
function randomHex() {
return '#' + Math.floor(Math.random() * 0xFFFFFF + 1)
.toString(16).padStart(6, '0').toUpperCase();
}
function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16);
return { r: (n >> 16) & 0xFF, g: (n >> 8) & 0xFF, b: n & 0xFF };
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0, s = 0;
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)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
}
function hslToHex(h, s, l) {
s /= 100; l /= 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));
return '#' + [f(0), f(8), f(4)].map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase();
}
// ── UI ───────────────────────────────────────────────────
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1800);
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
showToast(`Copied: ${text}`);
} catch {
showToast('Copy failed — try manually');
}
}
function setColor(hex) {
current = hex;
const { r, g, b } = hexToRgb(hex);
const { h, s, l } = rgbToHsl(r, g, b);
const rgb = `rgb(${r}, ${g}, ${b})`;
const hsl = `hsl(${h}, ${s}%, ${l}%)`;
preview.style.background = hex;
preview.textContent = '';
hexVal.textContent = hex;
rgbVal.textContent = rgb;
hslVal.textContent = hsl;
// Palette
palette.innerHTML = '';
[20, 35, 50, 65, 80].forEach(shade => {
const sw = document.createElement('div');
sw.className = 'swatch';
const shadeHex = hslToHex(h, s, shade);
sw.style.background = shadeHex;
sw.title = shadeHex;
sw.dataset.hex = shadeHex;
palette.appendChild(sw);
});
}
function renderSaved() {
const saved = getSaved();
savedShelf.innerHTML = '';
saved.forEach(hex => {
const sw = document.createElement('div');
sw.className = 'saved-swatch';
sw.style.background = hex;
sw.title = hex;
sw.dataset.hex = hex;
savedShelf.appendChild(sw);
});
}
function getSaved() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? []; }
catch { return []; }
}
// ── Events ───────────────────────────────────────────────
document.querySelector('#gen-btn').addEventListener('click', () => setColor(randomHex()));
preview.addEventListener('click', () => setColor(randomHex()));
document.querySelector('#save-btn').addEventListener('click', () => {
if (!current) return;
const saved = getSaved();
if (saved.includes(current)) { showToast('Already saved!'); return; }
const updated = [current, ...saved].slice(0, 12);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
renderSaved();
showToast('Saved!');
});
document.querySelector('#hex-box').addEventListener('click', () => copyText(hexVal.textContent));
document.querySelector('#rgb-box').addEventListener('click', () => copyText(rgbVal.textContent));
document.querySelector('#hsl-box').addEventListener('click', () => copyText(hslVal.textContent));
palette.addEventListener('click', e => {
const sw = e.target.closest('.swatch');
if (sw) setColor(sw.dataset.hex);
});
savedShelf.addEventListener('click', e => {
const sw = e.target.closest('.saved-swatch');
if (sw) setColor(sw.dataset.hex);
});
// Init
renderSaved();
setColor(randomHex());
</script>
</body>
</html>
Code Explained
Bitwise channel extraction
A HEX colour like #3B82F6 is 24 bits: 8 for red, 8 for green, 8 for blue. After parsing with parseInt(hex, 16) we get a single integer. Right-shifting by 16 (>> 16) moves the red bits to the low end, and & 0xFF masks off everything except the lowest 8 bits. The same mask without shifting extracts blue.
HSL for human-friendly palettes
HEX/RGB define colours as hardware channels. HSL (Hue, Saturation, Lightness) maps to human perception — Hue is the colour wheel position, Lightness is how light or dark. By keeping H and S constant and varying L from 20% to 80%, we get a set of shades that feel visually harmonious.
Clipboard API
navigator.clipboard.writeText() returns a Promise. It requires the page to be served over HTTPS (or localhost). Wrapping it in try/catch handles the case where the user denies clipboard permission.
Challenges
- Add a colour input (
<input type="color">) so users can pick any colour and see its HEX/RGB/HSL values instantly. - Generate complementary colours (opposite on the colour wheel: hue + 180°) and triadic colours (hue ± 120°).
- Add a contrast checker: given a background colour, calculate which text colour (black or white) has better WCAG contrast ratio.
- Add a gradient generator: pick two saved colours and output a CSS
linear-gradient()string. - Let users name their saved colours and export the palette as a CSS custom properties snippet.