Chapter 06 — Activity Guide

Build a Brand Palette Generator

Learn color theory math, HSL manipulation, and dynamic code generation — while building a real tool for designing brand color systems without any library.

Why This Project?

Color is math. Every design tool you've ever used — Figma, Coolors, Adobe Color — computes its palettes using the same trigonometry on the HSL color wheel. This chapter strips away the library and builds that math directly in vanilla JavaScript.

You'll learn to convert between hex and HSL, derive harmonic colors by rotating the hue angle, generate tint/shade scales via lightness interpolation, and export the result as CSS custom properties, SCSS variables, JSON, or a Tailwind config — all from the same underlying data array.

HSL Color Model

Hue (0–360°) is the color wheel position. Saturation (0–100%) is intensity. Lightness (0–100%) is brightness. Tints increase L; shades decrease L. Harmonics rotate H.

Hex ↔ HSL Conversion

Convert hex → RGB (divide by 255) → normalize to find min/max channel → compute H, S, L. Reverse: use the cubic formula from the CSS spec. No library needed — it's 15 lines.

Color Harmonics

Complementary = H + 180°. Triadic = H, H+120°, H+240°. Analogous = H±30°. Split-complementary = H+150°, H+210°. All are just modular arithmetic on the hue circle.

Dynamic Code Generation

The same palette object emits CSS, SCSS, JSON, or Tailwind config — just different string templates over the same key/value pairs. This is the factory pattern: one data model, many output formats.

HSL Color Math Hex Conversion Color Harmonics CSS Custom Properties Blob Download API SVG Generation Live Reactivity

The AI Angle for This Chapter

Color math is a precision task. The hex-to-HSL conversion must be exact — a small rounding error compounds through every derived color. This is a case where asking AI to "write a color converter" will give you working code, but you need to understand the algorithm well enough to verify it against known values.

Verify with known values before trusting generated math. Prompt: "Convert #e00000 to HSL. Show the calculation step by step." The answer should be hsl(0°, 100%, 44%). If AI gives you something different, its formula is wrong. Give it the expected output and ask it to fix the derivation — don't just accept the first result on color math.

“A color isn't a number. It's a position on a wheel.

The most powerful prompt technique for this chapter is describing the output format first. Write the exact CSS variables you want to see, then ask AI to write the function that generates them. The spec becomes your test case.

Before You Build — Think in HSL

Before writing any code, sketch the palette on paper using HSL values:

  1. Take any brand hex — say #e00000 (Internationally Tribal red). What is its H, S, L? Compute by hand or use browser DevTools color picker.
  2. What hex color is the complement? It's H+180°. If H=0, complement H=180 (cyan). Does that feel right visually?
  3. How would you generate 5 tints? You want to move L from the base value toward 95% in equal steps. Write the formula: L_tint_n = base_L + (95 - base_L) * (n / steps).
  4. If you want shades (darker), you move L toward 8%. Write that formula too. Notice it's the same structure with a different target.
  5. For neutral colors, you keep the same H but drop S to ~8% and vary L from dark to light. Why keep H at all instead of using pure gray (S=0)?

The math is simple linear interpolation and modular arithmetic. Everything else in this chapter is presentation around those two operations.

Build It

Phase 1

Build the hex ↔ HSL conversion utilities

These are the foundation. Every other function depends on them. Build hexToHsl(hex) and hslToHex(h,s,l) and test them manually before moving on.

Build hexToHsl(hex) that:
1. Parses the 6-digit hex into r, g, b (0–255 each)
2. Normalizes to 0–1 range
3. Finds max and min channel values
4. Computes L = (max+min)/2
5. Computes S = d / (1 - |2L-1|) where d = max-min
6. Computes H based on which channel is max
7. Returns [H (0-360), S (0-100), L (0-100)] as integers

Test: hexToHsl('#e00000') → [0, 100, 44]
Test: hexToHsl('#ffffff') → [0, 0, 100]
Test: hexToHsl('#0057b7') → [213, 100, 36]
Phase 2

Build scale and harmony generators

With conversion working, write getScale(hex, steps, satShift) that returns an array of color objects with hex and role, and getHarmony(hex, mode) that returns harmonic colors based on hue rotation.

getScale(hex, steps, satShift):
- Tints: for i from (steps-1) down to 1:
    L_tint = base_L + (95 - base_L) * (i / steps)
    Push { hex: hslToHex(H, S, L_tint), role: 'tint-i' }
- Push base color: { hex, role: 'base' }
- Shades: for i from 1 to steps-1:
    L_shade = base_L - (base_L - 8) * (i / steps)
    Push { hex: hslToHex(H, S, L_shade), role: 'shade-i' }
- satShift adjusts S on derived colors (clamp 0-100)

getHarmony(hex, mode): use a lookup object keyed by mode name.
Each value is an array of { hex: hslToHex(H+offset, S, L), role }.
Complementary offset: 180. Triadic: 0, 120, 240. Analogous: -30, 0, 30.
Phase 3

Build the swatch renderer

Render each color as a clickable swatch that shows hex + role. Clicking any swatch copies its hex to the clipboard using navigator.clipboard.writeText() and shows a brief "Copied!" overlay.

makeSwatch(color, showName, darkLabel):
1. Determine label text color: if L > 55 use dark (#111), else white
2. Create a div with background: color.hex
3. Inner label: hex value (bold) + role name (small)
4. On click: navigator.clipboard.writeText(color.hex),
   add class 'copied' for 1s (CSS ::after overlay: "Copied!")

For harmony colors use a card layout:
- Top half: color fill
- Bottom: role label, hex, hsl() string, optional name
Phase 4

Build multi-format code output

All colors from all three rows (scale, harmony, neutrals) should map to named variables. One buildCode(scale, harmony, neutrals, format, name) function handles CSS, SCSS, JSON, and Tailwind.

Build a flat array of [variableName, hexValue] pairs.
For CSS:
  ':root {\n' + pairs.map(([k,v]) => `  --${k}: ${v};`).join('\n') + '\n}'
For SCSS:
  pairs.map(([k,v]) => `$${k}: ${v};`).join('\n')
For JSON:
  JSON.stringify(Object.fromEntries(pairs), null, 2)
For Tailwind:
  Group pairs by prefix, nest under theme.extend.colors,
  output as module.exports JS string
Phase 5

Build SVG swatch sheet export

Generate a downloadable SVG showing all swatches as a labeled grid. Use the same Blob + Object URL download pattern from Chapter 05 — this time with type: 'image/svg+xml'.

downloadSVG():
1. Combine all colors into one flat array
2. Calculate grid dimensions: cols = min(length, 10), rows = ceil(length/cols)
3. For each color, emit:
   <rect x y width height rx fill={hex}/>
   <text>hex value</text>
   <text>role name</text>
4. Add palette title text at top
5. Wrap in <svg> with dark background fill
6. Blob with type 'image/svg+xml', download as 'brand-swatches.svg'

Concepts You Just Used

CSS custom properties are design tokens. The palette you export in this chapter is the same artifact that feeds Figma token plugins, Storybook theming, and component libraries. You just built the primitive layer of a full design system — without Figma, without Style Dictionary, without npm.

Reflect

  1. Why is HSL more useful than RGB for generating palettes? What would tint generation look like if you had to do it in RGB instead?
  2. The complementary of red (H=0°) is cyan (H=180°). Why does that combination feel so intense? What would you do to make it feel more harmonious in a real brand?
  3. This tool generates colors mathematically. A human designer would adjust them by feel. What's one thing a human would do that this tool can't — and how would you add a control for it?
  4. The neutral scale keeps the base hue at low saturation. How does that compare to using pure gray (s=0)? Which approach is better for brand consistency?
  5. The SVG export is a flat grid. How would you change the layout to a horizontal strip (one row per color group) with larger swatches?

You didn't install a color library. You wrote the math that is the color library. That's the difference between using tools and understanding them.

Go Further

🎨 Contrast Checker

Add WCAG contrast ratio calculation between any two selected swatches. Show pass/fail for AA (4.5:1) and AAA (7:1) standards. The formula uses relative luminance from the W3C spec.

🖼 Image Color Extraction

Use a <canvas> element to sample pixels from a dropped image. Average the top 5 most prominent hues and use one as the base color input automatically.

🌐 Palette History

Save up to 10 recent palettes in localStorage keyed by base hex + mode. Add a dropdown to reload any saved palette. Include a "pin" option to keep favorites.

📇 Figma Token Export

Figma Tokens plugin uses a specific JSON structure. Build a fifth export format that generates a tokens.json compatible with the plugin's import schema.

👀 Color Blindness Preview

Simulate deuteranopia and protanopia by applying the known color matrix transform to your palette swatches in a side-by-side preview panel.

🍀 Gradient Builder

Take any two swatches from your palette and generate a CSS linear-gradient between them with 5 intermediate stops. Preview it as a full-width banner and copy the CSS.

Up Next — Chapter 07

Markdown Live Editor

Build a split-pane Markdown editor with live preview, custom toolbar, and export to HTML or PDF. You'll learn DOM parsing, regex-based text transforms, and print-to-PDF from the browser.

Start Chapter 07 →