110
LVL 04 — SENIOR-IN-TRAININGSESSION 110DAY 110

COLOR PICKER

🎫 PIXELCRAFT-096
Feature | 🟡 Medium | Priority: 🟡 Medium

Users need to pick colors for drawing, text, and overlays. Build a full color picker: hue strip, saturation/value square, RGB/HSL inputs, eyedropper from canvas, recent colors palette, and an accessibility contrast indicator.
CONCEPTS.UNLOCKED
🎨
Color Spaces: RGB
Red, Green, Blue — how screens work. Each channel 0-255. White = (255,255,255). Black = (0,0,0). 16.7 million combinations. Great for screens, terrible for human intuition — what RGB makes "slightly warmer orange"?
🌈
Color Spaces: HSL / HSV
Hue, Saturation, Lightness/Value — how humans think. Hue = color wheel position (0-360°). Saturation = how vivid. Lightness/Value = how bright. Want warmer orange? Shift hue. Want pastel? Reduce saturation.
🔄
Color Conversion
HSV → RGB → Hex → HSL. Mathematical formulas convert between spaces. HSV is best for picking (intuitive). RGB is best for rendering (hardware). Hex is best for CSS (#ff6b2e).
💧
Eyedropper Tool
Click on the canvas → get the pixel color. ctx.getImageData(x, y, 1, 1) returns the RGBA values of one pixel. The most natural way to pick colors — point at what you want.
📐
Canvas Gradients
Render the picker UI on a canvas. The saturation/value square is two overlapping gradients: horizontal white→hue, vertical transparent→black. The hue strip is a linear gradient through all 360° hues.
Contrast Indicator
Show WCAG contrast ratio in real-time. As the user picks a color, calculate its contrast against white and black backgrounds. Show whether it passes AA (4.5:1) or AAA (7:1). Accessibility built into the tool.
HANDS-ON.TASKS
01
Color Conversion Functions
// lib/color.ts interface RGB { r: number; g: number; b: number } interface HSV { h: number; s: number; v: number } function hsvToRgb(hsv: HSV): RGB { const { h, s, v } = hsv; const c = v * s; const x = c * (1 - Math.abs( (h / 60) % 2 - 1)); const m = v - c; let r = 0, g = 0, b = 0; if (h < 60) { r = c; g = x; b = 0; } else if (h < 120) { r = x; g = c; b = 0; } else if (h < 180) { r = 0; g = c; b = x; } else if (h < 240) { r = 0; g = x; b = c; } else if (h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255), }; } function rgbToHex(rgb: RGB): string { const toHex = (n: number) => n.toString(16).padStart(2, '0'); return '#' + toHex(rgb.r) + toHex(rgb.g) + toHex(rgb.b); }
02
Saturation/Value Square
function drawSVSquare( ctx: CanvasRenderingContext2D, hue: number, width: number, height: number, ) { // Layer 1: white → hue (horizontal) const hueRgb = hsvToRgb({ h: hue, s: 1, v: 1 }); const hueColor = `rgb(${hueRgb.r},${hueRgb.g},` + `${hueRgb.b})`; const gradH = ctx.createLinearGradient( 0, 0, width, 0); gradH.addColorStop(0, '#fff'); gradH.addColorStop(1, hueColor); ctx.fillStyle = gradH; ctx.fillRect(0, 0, width, height); // Layer 2: transparent → black // (vertical) const gradV = ctx.createLinearGradient( 0, 0, 0, height); gradV.addColorStop(0, 'rgba(0,0,0,0)'); gradV.addColorStop(1, 'rgba(0,0,0,1)'); ctx.fillStyle = gradV; ctx.fillRect(0, 0, width, height); // Two gradients overlaid = // the full SV space for one hue. // Click position maps to (S, V). }
03
Hue Strip
function drawHueStrip( ctx: CanvasRenderingContext2D, width: number, height: number, ) { const grad = ctx.createLinearGradient( 0, 0, 0, height); // All 360° of hue const stops = [ [0, '#ff0000'], // 0° red [0.17, '#ffff00'], // 60° yellow [0.33, '#00ff00'], // 120° green [0.5, '#00ffff'], // 180° cyan [0.67, '#0000ff'], // 240° blue [0.83, '#ff00ff'], // 300° magenta [1, '#ff0000'], // 360° red ] as const; stops.forEach(([pos, color]) => grad.addColorStop(pos, color)); ctx.fillStyle = grad; ctx.fillRect(0, 0, width, height); // Click Y position → hue (0-360) }
04
Eyedropper Tool
function useEyedropper( canvasRef: RefObject<HTMLCanvasElement> ) { const pickColor = useCallback( (e: React.MouseEvent) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Get single pixel const pixel = ctx.getImageData(x, y, 1, 1).data; // pixel = [R, G, B, A] const rgb: RGB = { r: pixel[0], g: pixel[1], b: pixel[2], }; return { rgb, hex: rgbToHex(rgb), hsv: rgbToHsv(rgb), }; }, [canvasRef]); return { pickColor }; } // Also: EyeDropper API (Chrome 95+) // Built-in OS-level color picker // const dropper = new EyeDropper(); // const { sRGBHex } = // await dropper.open();
05
Contrast Indicator
// WCAG contrast ratio calculation function luminance(rgb: RGB): number { const [rs, gs, bs] = [rgb.r, rgb.g, rgb.b].map(c => { 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 contrastRatio( c1: RGB, c2: RGB ): number { const l1 = luminance(c1); const l2 = luminance(c2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } // Show in picker UI: // "vs White: 4.7:1 ✅ AA" // "vs Black: 3.2:1 ⚠️ Fails AA"
06
Close the Ticket
git switch -c feature/PIXELCRAFT-096-color-picker git add src/components/ColorPicker/ src/lib/color.ts git commit -m "Add full color picker with eyedropper (PIXELCRAFT-096)" git push origin feature/PIXELCRAFT-096-color-picker # PR → Review → Merge → Close ticket ✅
CS.DEEP-DIVE

Color spaces are different coordinate systems for the same reality.

Just as you can describe a location with (latitude, longitude) or (street, city), you can describe a color with (R, G, B) or (H, S, V) or (H, S, L). Each has strengths.

// Color space comparison:

RGB (Red, Green, Blue)
  How: hardware (screen pixels)
  Good for: rendering, storage
  Bad for: "make it warmer"

HSV (Hue, Saturation, Value)
  How: human perception
  Good for: color pickers, art
  H = which color (0-360°)
  S = how vivid (0-1)
  V = how bright (0-1)

HSL (Hue, Saturation, Lightness)
  How: CSS-friendly
  Good for: theming, variations
  L=50% is "pure", 0=black, 100=white

CMYK (Cyan, Magenta, Yellow, Key)
  How: printers (subtractive mixing)
  Good for: print design

// Converting between spaces is
// pure math — no data is lost
// (within the same gamut).
"Color Lab"
[A]Add color harmony suggestions: given a selected color, calculate complementary (180° on hue wheel), analogous (±30°), triadic (±120°), and split-complementary colors. Display as a palette strip below the picker.
[B]Add the native EyeDropper API (Chrome 95+): const dropper = new EyeDropper(); await dropper.open(). This gives OS-level color picking — sample any pixel on screen, not just the canvas. Feature-detect and fall back to canvas picking.
[C]Research: what is the OKLCH color space? It's perceptually uniform — equal numeric changes produce equal visual changes (unlike HSL where lightness isn't perceptually linear). Why is CSS adopting oklch() and color-mix()?
REF.MATERIAL
ARTICLE
MDN Web Docs
Complete reference for CSS colors: hex, rgb(), hsl(), oklch(), color-mix(), relative color syntax. The modern CSS color toolkit.
CSSCOLORESSENTIAL
VIDEO
Computerphile
RGB vs HSV vs HSL: how color spaces work, why screens use RGB, why humans prefer HSV, and the math to convert between them.
COLORTHEORY
ARTICLE
MDN Web Docs
Native OS-level color picking from any pixel on screen. Chrome 95+. The modern alternative to canvas-based eyedroppers.
EYEDROPPEROFFICIAL
ARTICLE
W3C
Official WCAG technique for contrast ratio calculation: relative luminance formula, minimum ratios for AA and AAA. The math behind accessibility.
CONTRASTA11Y
VIDEO
Kevin Powell
The new perceptually uniform color space coming to CSS: oklch(), color-mix(), and relative color syntax. Why it's better than HSL for design systems.
OKLCHMODERN
// LEAVE EXCITED BECAUSE
A full color picker built from scratch: hue wheel, SV square, RGB/HSL inputs, eyedropper, recent colors, and WCAG contrast indicator. You understand color at the math level — not just #ff6b2e but WHY it's orange.