P02: Hexadecimal Color Visualizer

P02: Hexadecimal Color Visualizer

Transform abstract hexadecimal notation into instant visual feedback Master the relationship between hex codes and the colors they represent


Table of Contents

  1. Learning Objectives
  2. Project Overview
  3. What You’ll Build
  4. Real World Outcome
  5. The Core Question You’re Answering
  6. Concepts You Must Understand First
  7. Questions to Guide Your Design
  8. Thinking Exercise
  9. The Interview Questions They’ll Ask
  10. Implementation Milestones
  11. Hints in Layers
  12. Books That Will Help
  13. Self-Assessment Checklist

Learning Objectives

By completing this project, you will be able to:

  1. Convert hexadecimal to decimal fluently - Look at FF and immediately know it means 255
  2. Understand the RGB color model - Explain why mixing red and green light produces yellow
  3. Parse and validate hex color strings - Handle #FF5733, FF5733, and #F53 correctly
  4. Manipulate the DOM in real-time - Update visual elements as users type
  5. Implement bidirectional conversion - Go from hex to RGB and RGB to hex seamlessly
  6. Debug color-related issues - Quickly identify why a color looks wrong
  7. Apply this knowledge to CSS, design tools, and graphics programming

Project Overview

Attribute Value
Main Programming Language JavaScript (with HTML/CSS)
Alternative Languages Python with Tkinter, Python with PyQt, Rust with egui
Difficulty Beginner
Time Estimate A few hours
Prerequisites Basic HTML, CSS, and JavaScript fundamentals
Core Concept Hexadecimal in Visual Context (RGB Color Model)

What You’ll Build

A web page with a text input for hex color codes and a large div that displays that color. As the user types a valid hex code, the color swatch updates in real-time.

+------------------------------------------------------------------+
|                    HEX COLOR VISUALIZER                           |
+------------------------------------------------------------------+
|                                                                   |
|   Enter Hex Color: [#FF5733         ]                             |
|                                                                   |
|   +----------------------------------------------------------+   |
|   |                                                          |   |
|   |                                                          |   |
|   |                                                          |   |
|   |                   COLOR PREVIEW                          |   |
|   |                                                          |   |
|   |                 (Large color swatch)                     |   |
|   |                                                          |   |
|   |                                                          |   |
|   +----------------------------------------------------------+   |
|                                                                   |
|   RGB Breakdown:                                                  |
|   +------------------+  +------------------+  +------------------+|
|   | R: FF = 255      |  | G: 57 = 87       |  | B: 33 = 51       ||
|   +------------------+  +------------------+  +------------------+|
|                                                                   |
|   [Optional: RGB Sliders]                                         |
|   Red:   [===================|-------] 255                        |
|   Green: [=======|-------------------] 87                         |
|   Blue:  [====|----------------------] 51                         |
|                                                                   |
+------------------------------------------------------------------+

Real World Outcome

When you complete this project, you’ll have a fully functional color visualization tool. Here’s exactly what you’ll see:

The Interface

Large Color Swatch Behavior:

  • Starts with a default color (white #FFFFFF or a nice neutral)
  • Instantly updates as the user types valid hex codes
  • Smooth transition between colors (optional CSS enhancement)
  • Takes up at least 300x200 pixels so the color is clearly visible

Input Field Behavior:

  • Accepts input with or without the # prefix
  • Handles both 6-character (FF5733) and 3-character (F53) shorthand
  • Shows visual feedback for invalid input (red border, warning message)
  • Allows uppercase, lowercase, or mixed case

RGB Value Display:

  • Shows the hex pair for each channel: R: FF, G: 57, B: 33
  • Shows the decimal equivalent: = 255, = 87, = 51
  • Updates synchronously with the color swatch

RGB Sliders (Optional Enhancement):

  • Three range inputs for R, G, B (0-255 each)
  • Moving a slider updates both the color swatch AND the hex input
  • Demonstrates bidirectional conversion

Real-Time Updates

User types: "#FF"
-> Input valid so far, but incomplete
-> Color swatch shows nothing (or stays on previous color)

User types: "#FF5"
-> Still incomplete for 6-character format
-> Could be valid 3-character shorthand (#FF5 = #FFFF55)

User types: "#FF57"
-> Invalid (not 3 or 6 characters after #)
-> Show validation warning

User types: "#FF5733"
-> Valid! Color swatch shows coral/orange-red
-> RGB display shows: R:255, G:87, B:51

Error Handling Visual Feedback

Invalid Input: "#GGGGGG"
+------------------------------------------------------------------+
|   Enter Hex Color: [#GGGGGG        ] <-- Red border              |
|   [!] Invalid hex color code                                     |
+------------------------------------------------------------------+

Valid Input: "#00FF00"
+------------------------------------------------------------------+
|   Enter Hex Color: [#00FF00        ] <-- Green border (success)  |
|   Pure green - nice choice!                                      |
+------------------------------------------------------------------+

The “Aha!” Moment

When you finish this project, you’ll experience the moment where hex colors “click”:

  • You’ll see #FF0000 and instantly know it’s pure red
  • You’ll look at #00FF00 and recognize pure green without thinking
  • You’ll understand why #808080 is exactly 50% gray
  • You’ll see #FFFFFF and know it’s white because all channels are maxed out
  • You’ll realize that #000000 is black because all light is absent

This intuition is invaluable for web development, design, and understanding digital imagery.


The Core Question You’re Answering

“How does a computer represent 16.7 million colors using just six characters, and why is #FF5733 the same as RGB(255, 87, 51)?”

Let’s break this down:

Why 16.7 Million Colors?

Each channel (R, G, B) uses 8 bits (1 byte)
8 bits can represent 2^8 = 256 different values (0-255)

Total combinations:
256 red values x 256 green values x 256 blue values
= 256^3
= 16,777,216 colors

This is written in hex as:
#000000 (black) to #FFFFFF (white)

Why Hex Instead of Decimal?

Decimal representation: rgb(255, 87, 51)
- Requires three separate numbers
- 3-12 characters depending on values
- Hard to parse at a glance

Hex representation: #FF5733
- Always exactly 6 characters (plus #)
- Each pair maps directly to one channel
- Compact and consistent

The key insight: 2 hex digits = 1 byte = values 0-255
FF in hex = 15*16 + 15 = 255 in decimal
00 in hex = 0*16 + 0 = 0 in decimal

The Conversion Formula

Hex pair to decimal:
   [H1][H2] = (H1 * 16) + H2

Example: "5A"
   5 * 16 = 80
   A (10) = 10
   Result: 80 + 10 = 90

Decimal to hex pair:
   D / 16 = H1 (first hex digit)
   D % 16 = H2 (second hex digit)

Example: 90
   90 / 16 = 5 (integer division)
   90 % 16 = 10 = A
   Result: "5A"

Concepts You Must Understand First

1. The RGB Color Model (Additive Color Mixing)

Unlike paint (which uses subtractive mixing), light uses additive color mixing.

    RED + GREEN = YELLOW
    RED + BLUE = MAGENTA
    GREEN + BLUE = CYAN
    RED + GREEN + BLUE = WHITE

         RED (R)
           |
           |
   YELLOW  |  MAGENTA
      \    |    /
       \   |   /
        \  |  /
         \ | /
          \|/
           +------------- GREEN (G)
          /|\
         / | \
        /  |  \
       /   |   \
      /    |    \
   CYAN         (no mix)
           |
           |
        BLUE (B)

Why RGB? Human eyes have three types of color receptors (cones) that are most sensitive to red, green, and blue wavelengths. By mixing these three “primary” colors of light at different intensities, we can trick the eye into seeing virtually any color.

Book Reference:

  • “The Nature of Code” by Daniel Shiffman, Chapter on Color (explains RGB in the context of programming)
  • “Code” by Charles Petzold, Chapter 25 (The Graphical Revolution)

2. 8-Bit Color Depth (0-255 Range)

Each color channel uses 8 bits, which gives us 256 possible intensity levels:

8 bits = 2^8 = 256 values

Binary:  00000000  to  11111111
Decimal:        0  to       255
Hex:           00  to        FF

This means:
- 0 (00) = No light from this channel
- 255 (FF) = Maximum light from this channel
- 128 (80) = Approximately 50% intensity

Why 8 bits per channel?

  • 8 bits = 1 byte = convenient for computer memory
  • 256 levels per channel gives smooth gradients
  • Human eyes can’t distinguish more than ~10 million colors anyway

Book Reference:

  • “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron, Section 2.1 (Information Storage)

3. Hexadecimal as Byte Representation

Hexadecimal (base 16) is the perfect notation for bytes because:

1 hex digit = 4 bits (a "nibble")
2 hex digits = 8 bits (a byte)

Hex Digit | Binary | Decimal
----------|--------|--------
    0     | 0000   |    0
    1     | 0001   |    1
    2     | 0010   |    2
    3     | 0011   |    3
    4     | 0100   |    4
    5     | 0101   |    5
    6     | 0110   |    6
    7     | 0111   |    7
    8     | 1000   |    8
    9     | 1001   |    9
    A     | 1010   |   10
    B     | 1011   |   11
    C     | 1100   |   12
    D     | 1101   |   13
    E     | 1110   |   14
    F     | 1111   |   15

Why is this important? When you see #FF, you’re seeing the binary value 11111111 written compactly. Each hex digit represents exactly 4 bits, making conversion straightforward.

Book Reference:

  • “Code” by Charles Petzold, Chapters 7-8 (covers number systems in depth)

4. The Hex Color Format (#RRGGBB)

The standard 6-character hex color format packs three bytes into one string:

    #  FF   57   33
       |    |    |
       |    |    +-- Blue channel (0x33 = 51)
       |    +------- Green channel (0x57 = 87)
       +------------ Red channel (0xFF = 255)

Memory layout (3 bytes):
+--------+--------+--------+
|   FF   |   57   |   33   |  = 24 bits total
+--------+--------+--------+
  Red      Green    Blue

Shorthand Notation (#RGB):

#F53 expands to #FF5533
Each single digit is doubled:
  F -> FF
  5 -> 55
  3 -> 33

5. String Parsing and Validation

A valid hex color string must satisfy these rules:

Rule 1: Optional # prefix
Rule 2: Exactly 3 or 6 hex characters after the prefix
Rule 3: Each character must be in [0-9, A-F, a-f]

Valid examples:
  #FF5733, FF5733, #f53, f53, #abc123, ABC123

Invalid examples:
  #GGGGGG (G is not a hex digit)
  #12345  (5 characters - neither 3 nor 6)
  #12345678 (8 characters - too long)
  12G     (G is not a hex digit)

Regular Expression for Validation:

// Matches #RGB, RGB, #RRGGBB, or RRGGBB
/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/

6. Converting Hex to Decimal Programmatically

The conversion uses the positional value of each digit:

Hexadecimal is base 16, so each position is worth 16^n

For a two-digit hex value like "5A":
  Position 1 (5): 5 * 16^1 = 5 * 16 = 80
  Position 0 (A): 10 * 16^0 = 10 * 1 = 10
  Total: 80 + 10 = 90

In JavaScript:
  parseInt("5A", 16) === 90
  parseInt("FF", 16) === 255
  parseInt("00", 16) === 0

Book Reference:

  • “Eloquent JavaScript” by Marijn Haverbeke, Chapter 4 (Data Structures) covers parsing

7. DOM Manipulation and Event Handling

To build this visualizer, you need to understand:

// Getting elements
const input = document.getElementById('hexInput');
const swatch = document.getElementById('colorSwatch');

// Listening for input events
input.addEventListener('input', (event) => {
  const value = event.target.value;
  // Update the swatch
});

// Changing element styles
swatch.style.backgroundColor = '#FF5733';

// Adding/removing CSS classes
input.classList.add('invalid');
input.classList.remove('invalid');

Book Reference:

  • “JavaScript: The Definitive Guide” by David Flanagan, Chapter 15 (Scripting Documents)

Questions to Guide Your Design

Before writing code, think through these design questions:

Input Handling

  1. Should the input field start with a # already in it, or should users add it themselves?
  2. What happens if the user types lowercase letters? (Should ff0000 work the same as FF0000?)
  3. Should you validate on every keystroke, or wait until the user finishes typing?
  4. How will you handle the 3-character shorthand (#RGB)?

Color Display

  1. What should the initial color be when the page loads?
  2. How large should the color swatch be to show the color clearly?
  3. Should the color transition smoothly, or change instantly?
  4. What color should the swatch show while the user is typing an incomplete hex code?

RGB Conversion

  1. How will you extract the R, G, and B values from a 6-character hex string?
  2. How will you expand a 3-character shorthand to 6 characters?
  3. Should you display the decimal values alongside the hex values?

User Experience

  1. How will you indicate invalid input? (Red border? Error message? Disabled preview?)
  2. Should you allow pasting colors from the clipboard?
  3. What keyboard shortcuts might be useful?

Enhancement Ideas

  1. If you add RGB sliders, how will changes to sliders update the hex input?
  2. Should you show complementary or analogous colors?
  3. Could you add a color picker eyedropper tool?

Thinking Exercise

Before you write any code, work through these conversions by hand on paper. This builds the mental model that makes hex colors intuitive.

Exercise 1: Hex to RGB Conversion

Given: #A3C2F1

Break it into components:

Step 1: Identify the pairs
  R: A3
  G: C2
  B: F1

Step 2: Convert each pair to decimal

  A3:
    A = 10 (in decimal)
    3 = 3
    A3 = (10 * 16) + 3 = 160 + 3 = 163

  C2:
    C = 12
    2 = 2
    C2 = (12 * 16) + 2 = 192 + 2 = 194

  F1:
    F = 15
    1 = 1
    F1 = (15 * 16) + 1 = 240 + 1 = 241

Step 3: Write the RGB value
  RGB(163, 194, 241)

Your turn: What color would this be? (Hint: B is highest, R and G are moderate)

Answer

This is a light sky blue - the high blue value combined with moderate red and green creates a soft, sky-like color.

Exercise 2: RGB to Hex Conversion

Given: RGB(255, 87, 51)

Convert each decimal to hex:

Step 1: Red = 255
  255 / 16 = 15 remainder 15
  First digit: 15 = F
  Second digit: 15 = F
  Red hex: FF

Step 2: Green = 87
  87 / 16 = 5 remainder 7
  First digit: 5
  Second digit: 7
  Green hex: 57

Step 3: Blue = 51
  51 / 16 = 3 remainder 3
  First digit: 3
  Second digit: 3
  Blue hex: 33

Step 4: Combine
  #FF5733

Your turn: Verify this is a coral/orange-red by noting which channels are dominant.

Answer

Red (255) is maxed out, Green (87) is about 1/3, Blue (51) is about 1/5. This creates a warm coral/orange-red color with strong red influence.

Exercise 3: Shorthand Expansion

Given: #F80

Expand each character:
  F -> FF
  8 -> 88
  0 -> 00

Result: #FF8800

This is a pure orange (max red, medium-high green, no blue)

Exercise 4: Special Colors Recognition

Fill in the blank:

#FF0000 = Pure _____ (max red, no green, no blue)
#00FF00 = Pure _____ (no red, max green, no blue)
#0000FF = Pure _____ (no red, no green, max blue)
#FFFF00 = _____ (max red, max green, no blue)
#00FFFF = _____ (no red, max green, max blue)
#FF00FF = _____ (max red, no green, max blue)
#000000 = _____ (all channels zero)
#FFFFFF = _____ (all channels max)
#808080 = _____ (all channels at 128/256 = 50%)
Answers
  • #FF0000 = Pure RED
  • #00FF00 = Pure GREEN (also called “lime”)
  • #0000FF = Pure BLUE
  • #FFFF00 = YELLOW (red + green)
  • #00FFFF = CYAN (green + blue)
  • #FF00FF = MAGENTA (red + blue)
  • #000000 = BLACK (no light)
  • #FFFFFF = WHITE (all light)
  • #808080 = GRAY (50% gray, middle of the spectrum)

The Interview Questions They’ll Ask

These are real questions from technical interviews related to colors, hex, and bit manipulation:

Basic Understanding

Q1: “Why do we use hexadecimal for colors instead of decimal?”

Expected answer: Hexadecimal maps perfectly to bytes. Each hex digit represents 4 bits, so two hex digits (like FF) represent exactly one byte (8 bits). This makes it easy to see that each color channel (R, G, B) is one byte. It’s also more compact: #FFFFFF vs rgb(255, 255, 255).

Q2: “How many colors can be represented with 24-bit color?”

Expected answer: 2^24 = 16,777,216 colors. This comes from 8 bits per channel times 3 channels = 24 bits total.

Q3: “What’s the difference between #RGB and #RRGGBB?”

Expected answer: #RGB is shorthand where each digit is doubled. #F53 becomes #FF5533. It can only represent 4,096 colors (16^3) compared to 16.7 million for the full format.

Intermediate

Q4: “How would you convert the hex value ‘B7’ to decimal without using parseInt?”

Expected answer:

const hex = 'B7';
const decimal = (parseInt(hex[0], 16) * 16) + parseInt(hex[1], 16);
// Or manually: ('B' = 11) * 16 + ('7' = 7) = 176 + 7 = 183

Q5: “How would you invert a color given its hex code?”

Expected answer: Subtract each channel from 255 (or FF).

function invertColor(hex) {
  const r = 255 - parseInt(hex.slice(1, 3), 16);
  const g = 255 - parseInt(hex.slice(3, 5), 16);
  const b = 255 - parseInt(hex.slice(5, 7), 16);
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}

Q6: “How would you calculate the luminance/brightness of a color?”

Expected answer: Use the formula that accounts for human perception:

// Perceived brightness formula (ITU-R BT.709)
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
// Or simpler approximation: (R + R + B + G + G + G) / 6

Advanced

Q7: “Why does mixing red and green light make yellow, but mixing red and green paint makes brown?”

Expected answer: Light uses additive color mixing (RGB) - adding more light makes colors brighter toward white. Paint uses subtractive color mixing (CMYK) - adding more pigment absorbs more light, making colors darker toward black. Yellow light is our perception of red + green wavelengths. Brown paint results from red + green pigments each absorbing different wavelengths.

Q8: “How would you determine if text should be black or white based on a background color?”

Expected answer: Calculate the luminance of the background and choose high contrast text:

function getContrastColor(hexcolor) {
  const r = parseInt(hexcolor.slice(1, 3), 16);
  const g = parseInt(hexcolor.slice(3, 5), 16);
  const b = parseInt(hexcolor.slice(5, 7), 16);
  const luminance = (r * 299 + g * 587 + b * 114) / 1000;
  return luminance > 128 ? '#000000' : '#FFFFFF';
}

Q9: “What is color depth, and why might 16-bit color look different from 24-bit?”

Expected answer: Color depth is the number of bits used to represent a color. 24-bit uses 8 bits per channel (16.7M colors). 16-bit typically uses 5-6-5 (5 bits red, 6 bits green, 5 bits blue = 65,536 colors). With fewer colors, gradients may show visible banding.


Implementation Milestones

Progress through these milestones to build understanding systematically:

Milestone 1: Static HTML Structure

Goal: Create the page layout without any JavaScript.

Deliverable:

  • HTML file with input field and color swatch div
  • Basic CSS styling
  • Static placeholder color

Demonstrates understanding of:

  • HTML document structure
  • CSS styling for layout
  • Semantic element selection

Milestone 2: Parse and Validate Hex Input

Goal: Accept input and validate it as a proper hex color.

Deliverable:

  • JavaScript that reads input field value
  • Validation function that returns true/false
  • Console.log showing validation results

Demonstrates understanding of:

  • String manipulation
  • Regular expressions (optional)
  • Character classification (hex digits)

Milestone 3: Convert Hex to Decimal RGB

Goal: Extract R, G, B decimal values from a valid hex string.

Deliverable:

  • Function that takes “#FF5733” and returns {r: 255, g: 87, b: 51}
  • Handle both 6-character and 3-character formats
  • Console.log showing conversions

Demonstrates understanding of:

  • Hexadecimal to decimal conversion
  • String slicing
  • Object construction

Milestone 4: Update Color Swatch

Goal: Change the swatch color based on input.

Deliverable:

  • Color swatch updates as user types
  • Only updates for valid hex codes
  • Smooth or instant color transition

Demonstrates understanding of:

  • DOM manipulation
  • Event listeners
  • CSS background-color property

Milestone 5: Display RGB Values

Goal: Show the breakdown of R, G, B values.

Deliverable:

  • Display area showing “R: FF = 255”, etc.
  • Updates synchronously with swatch
  • Clear visual hierarchy

Demonstrates understanding of:

  • Dynamic content updates
  • Formatting numbers
  • Template literals or string concatenation

Milestone 6: Add RGB Sliders (Optional Enhancement)

Goal: Allow color selection via sliders that sync with hex input.

Deliverable:

  • Three range inputs (0-255)
  • Moving sliders updates hex input and swatch
  • Editing hex input updates slider positions
  • Bidirectional synchronization works correctly

Demonstrates understanding of:

  • Input type=”range” elements
  • Two-way data binding concepts
  • Decimal to hexadecimal conversion
  • Avoiding infinite update loops

Milestone 7: Polish and Error Handling

Goal: Professional-quality user experience.

Deliverable:

  • Visual indication of invalid input
  • Helpful error messages
  • Edge case handling (empty input, special characters)
  • Keyboard accessibility

Demonstrates understanding of:

  • UX best practices
  • Error state management
  • Accessibility considerations

Hints in Layers

Each layer provides progressively more guidance. Try to complete each step before revealing the next hint.

Layer 1: Start with Static HTML

Click to reveal

Create the basic structure first. Don’t worry about JavaScript yet.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hex Color Visualizer</title>
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
    }

    .color-swatch {
      width: 100%;
      height: 300px;
      border: 2px solid #333;
      border-radius: 8px;
      margin: 20px 0;
      transition: background-color 0.2s ease;
    }

    .hex-input {
      font-size: 24px;
      padding: 10px 15px;
      font-family: monospace;
      width: 200px;
      text-transform: uppercase;
    }

    .rgb-display {
      display: flex;
      gap: 20px;
      margin-top: 20px;
    }

    .channel {
      padding: 10px 15px;
      background: #f0f0f0;
      border-radius: 4px;
      font-family: monospace;
    }
  </style>
</head>
<body>
  <h1>Hex Color Visualizer</h1>

  <label for="hexInput">Enter Hex Color:</label>
  <input
    type="text"
    id="hexInput"
    class="hex-input"
    value="#FF5733"
    maxlength="7"
  >

  <div id="colorSwatch" class="color-swatch" style="background-color: #FF5733;"></div>

  <div class="rgb-display">
    <span class="channel" id="redChannel">R: FF = 255</span>
    <span class="channel" id="greenChannel">G: 57 = 87</span>
    <span class="channel" id="blueChannel">B: 33 = 51</span>
  </div>

  <script>
    // JavaScript will go here
  </script>
</body>
</html>

Layer 2: Parse the Hex String

Click to reveal

Write a function to parse and normalize the hex input:

function parseHexColor(input) {
  // Remove whitespace and # prefix
  let hex = input.trim();
  if (hex.startsWith('#')) {
    hex = hex.slice(1);
  }

  // Convert to uppercase for consistency
  hex = hex.toUpperCase();

  // Handle 3-character shorthand
  if (hex.length === 3) {
    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  }

  // Validate: must be exactly 6 hex characters
  if (hex.length !== 6) {
    return null;
  }

  // Check each character is a valid hex digit
  const validHex = /^[0-9A-F]{6}$/;
  if (!validHex.test(hex)) {
    return null;
  }

  return hex;
}

// Test it
console.log(parseHexColor('#FF5733'));  // "FF5733"
console.log(parseHexColor('ff5733'));   // "FF5733"
console.log(parseHexColor('#F53'));     // "FF5533"
console.log(parseHexColor('invalid'));  // null

Layer 3: Convert Hex to Decimal

Click to reveal

Extract RGB values from the normalized hex string:

function hexToRgb(hex) {
  // hex should be a 6-character string like "FF5733"
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);

  return { r, g, b };
}

// Test it
const rgb = hexToRgb('FF5733');
console.log(rgb);  // { r: 255, g: 87, b: 51 }

// Manual calculation verification:
// FF = 15*16 + 15 = 255 ✓
// 57 = 5*16 + 7 = 87 ✓
// 33 = 3*16 + 3 = 51 ✓

If you want to understand parseInt better, here’s the manual version:

function hexCharToDecimal(char) {
  const code = char.charCodeAt(0);
  if (code >= 48 && code <= 57) {  // '0'-'9'
    return code - 48;
  }
  if (code >= 65 && code <= 70) {  // 'A'-'F'
    return code - 65 + 10;
  }
  if (code >= 97 && code <= 102) { // 'a'-'f'
    return code - 97 + 10;
  }
  return -1;  // Invalid character
}

function hexPairToDecimal(pair) {
  const high = hexCharToDecimal(pair[0]);
  const low = hexCharToDecimal(pair[1]);
  return high * 16 + low;
}

// Test: hexPairToDecimal('FF') === 255

Layer 4: Update the Color Swatch

Click to reveal

Connect the input to the swatch:

const hexInput = document.getElementById('hexInput');
const colorSwatch = document.getElementById('colorSwatch');

function updateColor() {
  const input = hexInput.value;
  const hex = parseHexColor(input);

  if (hex) {
    // Valid hex - update the swatch
    colorSwatch.style.backgroundColor = '#' + hex;
    hexInput.classList.remove('invalid');
    hexInput.classList.add('valid');
  } else {
    // Invalid hex - show error state
    hexInput.classList.remove('valid');
    hexInput.classList.add('invalid');
    // Optionally keep the last valid color
  }
}

// Listen for input events (fires on every keystroke)
hexInput.addEventListener('input', updateColor);

// Also update on page load
updateColor();

Add CSS for the validation states:

.hex-input.valid {
  border-color: #22c55e;
  outline-color: #22c55e;
}

.hex-input.invalid {
  border-color: #ef4444;
  outline-color: #ef4444;
}

Layer 5: Display RGB Values

Click to reveal

Update the RGB display when the color changes:

const redChannel = document.getElementById('redChannel');
const greenChannel = document.getElementById('greenChannel');
const blueChannel = document.getElementById('blueChannel');

function updateRgbDisplay(hex) {
  const rgb = hexToRgb(hex);

  // Format: "R: FF = 255"
  redChannel.textContent = `R: ${hex.slice(0, 2)} = ${rgb.r}`;
  greenChannel.textContent = `G: ${hex.slice(2, 4)} = ${rgb.g}`;
  blueChannel.textContent = `B: ${hex.slice(4, 6)} = ${rgb.b}`;
}

// Modify updateColor to also update the RGB display
function updateColor() {
  const input = hexInput.value;
  const hex = parseHexColor(input);

  if (hex) {
    colorSwatch.style.backgroundColor = '#' + hex;
    updateRgbDisplay(hex);  // Add this line
    hexInput.classList.remove('invalid');
    hexInput.classList.add('valid');
  } else {
    hexInput.classList.remove('valid');
    hexInput.classList.add('invalid');
  }
}

Layer 6: Add RGB Sliders (Optional)

Click to reveal

Add bidirectional conversion with sliders:

<!-- Add to HTML -->
<div class="sliders">
  <div class="slider-row">
    <label for="redSlider">R:</label>
    <input type="range" id="redSlider" min="0" max="255" value="255">
    <span id="redValue">255</span>
  </div>
  <div class="slider-row">
    <label for="greenSlider">G:</label>
    <input type="range" id="greenSlider" min="0" max="255" value="87">
    <span id="greenValue">87</span>
  </div>
  <div class="slider-row">
    <label for="blueSlider">B:</label>
    <input type="range" id="blueSlider" min="0" max="255" value="51">
    <span id="blueValue">51</span>
  </div>
</div>
const redSlider = document.getElementById('redSlider');
const greenSlider = document.getElementById('greenSlider');
const blueSlider = document.getElementById('blueSlider');

// Convert decimal to 2-digit hex
function toHexPair(decimal) {
  return decimal.toString(16).padStart(2, '0').toUpperCase();
}

// Convert RGB to hex string
function rgbToHex(r, g, b) {
  return toHexPair(r) + toHexPair(g) + toHexPair(b);
}

// Flag to prevent infinite loops
let isUpdating = false;

function updateFromSliders() {
  if (isUpdating) return;
  isUpdating = true;

  const r = parseInt(redSlider.value);
  const g = parseInt(greenSlider.value);
  const b = parseInt(blueSlider.value);

  const hex = rgbToHex(r, g, b);
  hexInput.value = '#' + hex;
  colorSwatch.style.backgroundColor = '#' + hex;
  updateRgbDisplay(hex);

  // Update value labels
  document.getElementById('redValue').textContent = r;
  document.getElementById('greenValue').textContent = g;
  document.getElementById('blueValue').textContent = b;

  hexInput.classList.remove('invalid');
  hexInput.classList.add('valid');

  isUpdating = false;
}

function updateSliders(hex) {
  if (isUpdating) return;
  isUpdating = true;

  const rgb = hexToRgb(hex);
  redSlider.value = rgb.r;
  greenSlider.value = rgb.g;
  blueSlider.value = rgb.b;

  document.getElementById('redValue').textContent = rgb.r;
  document.getElementById('greenValue').textContent = rgb.g;
  document.getElementById('blueValue').textContent = rgb.b;

  isUpdating = false;
}

// Add event listeners
redSlider.addEventListener('input', updateFromSliders);
greenSlider.addEventListener('input', updateFromSliders);
blueSlider.addEventListener('input', updateFromSliders);

// Modify updateColor to also update sliders
function updateColor() {
  if (isUpdating) return;

  const input = hexInput.value;
  const hex = parseHexColor(input);

  if (hex) {
    colorSwatch.style.backgroundColor = '#' + hex;
    updateRgbDisplay(hex);
    updateSliders(hex);  // Add this line
    hexInput.classList.remove('invalid');
    hexInput.classList.add('valid');
  } else {
    hexInput.classList.remove('valid');
    hexInput.classList.add('invalid');
  }
}

Layer 7: Add Better Validation and UX

Click to reveal

Add helpful error messages and polish:

<!-- Add error message element -->
<div id="errorMessage" class="error-message"></div>
.error-message {
  color: #ef4444;
  font-size: 14px;
  margin-top: 8px;
  min-height: 20px;
}
const errorMessage = document.getElementById('errorMessage');

function validateAndShowErrors(input) {
  let hex = input.trim();

  // Check for empty input
  if (hex === '' || hex === '#') {
    errorMessage.textContent = '';
    return null;
  }

  // Remove # if present
  if (hex.startsWith('#')) {
    hex = hex.slice(1);
  }

  // Check length
  if (hex.length !== 3 && hex.length !== 6) {
    if (hex.length < 6) {
      errorMessage.textContent = 'Keep typing... need 3 or 6 characters';
    } else {
      errorMessage.textContent = 'Too long! Hex colors are 3 or 6 characters';
    }
    return null;
  }

  // Check for invalid characters
  const invalidChars = hex.match(/[^0-9A-Fa-f]/g);
  if (invalidChars) {
    const unique = [...new Set(invalidChars)];
    errorMessage.textContent = `Invalid character(s): ${unique.join(', ')}`;
    return null;
  }

  // Valid!
  errorMessage.textContent = '';

  // Expand shorthand
  if (hex.length === 3) {
    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  }

  return hex.toUpperCase();
}

// Replace parseHexColor in updateColor with validateAndShowErrors
function updateColor() {
  if (isUpdating) return;

  const input = hexInput.value;
  const hex = validateAndShowErrors(input);

  if (hex) {
    colorSwatch.style.backgroundColor = '#' + hex;
    updateRgbDisplay(hex);
    updateSliders(hex);
    hexInput.classList.remove('invalid');
    hexInput.classList.add('valid');
  } else if (hexInput.value.trim() !== '' && hexInput.value.trim() !== '#') {
    hexInput.classList.remove('valid');
    hexInput.classList.add('invalid');
  } else {
    // Empty or just # - neutral state
    hexInput.classList.remove('valid', 'invalid');
  }
}

Books That Will Help

Topic Book Chapter/Section What You’ll Learn
Number Systems “Code” by Charles Petzold Chapters 7-8 Deep understanding of binary, hex, and why they matter
RGB Color Theory “The Nature of Code” by Daniel Shiffman Color chapter How RGB works in programming contexts
JavaScript DOM “Eloquent JavaScript” by Marijn Haverbeke Chapter 14 DOM manipulation fundamentals
JavaScript Events “JavaScript: The Definitive Guide” by David Flanagan Chapter 15 Event handling patterns
CSS Fundamentals “CSS: The Definitive Guide” by Eric Meyer Color chapter CSS color formats and conversions
Bit Manipulation “CSAPP” by Bryant & O’Hallaron Section 2.1 How bytes and bits work at the hardware level
Color Science “Color Science” by Wyszecki & Stiles Chapter 3 The physics of human color perception (advanced)

Quick References

For hex color basics:

  • MDN Web Docs: “Using CSS color” (online reference)
  • CSS-Tricks: “HSL color wheel” (for understanding color spaces)

For JavaScript:

  • JavaScript.info: “Document” section (DOM tutorials)
  • MDN: parseInt() documentation

Self-Assessment Checklist

Before considering this project complete, verify:

Conceptual Understanding

  • I can look at #FF0000 and immediately know it’s red without calculating
  • I can explain why #808080 is exactly 50% gray
  • I understand why there are 16.7 million possible colors in 24-bit color
  • I can convert any 2-digit hex value to decimal in my head (e.g., 7F = 127)
  • I can explain why mixing red and green light makes yellow

Implementation Skills

  • My visualizer correctly parses both #RRGGBB and #RGB formats
  • Invalid input shows appropriate error feedback
  • The color swatch updates in real-time as the user types
  • RGB decimal values display correctly for any valid hex color
  • Case doesn’t matter (ff0000 works the same as FF0000)

Edge Cases Handled

  • Empty input doesn’t cause errors
  • Partial input (like “#FF5”) shows appropriate state
  • Invalid characters (like G, H, etc.) are caught
  • Extra characters (more than 6) are handled properly

Code Quality

  • Functions are small and do one thing
  • Variable names clearly indicate purpose
  • No global variable pollution (use const/let, not var)
  • Event listeners are properly attached

Bonus Achievements (Optional)

  • RGB sliders work bidirectionally with hex input
  • Color transitions are smooth (CSS transition)
  • Complementary color is displayed
  • “Copy to clipboard” button works
  • Contrast ratio is calculated and displayed

Extension Ideas

Once you’ve completed the basic visualizer, consider these enhancements:

1. Color Harmony Display

Show complementary, analogous, and triadic colors based on the current selection.

2. Color Blindness Simulator

Show how the color appears to people with different types of color blindness (protanopia, deuteranopia, tritanopia).

3. Named Color Lookup

If the hex matches a CSS named color (like “coral” or “tomato”), display that name.

4. Color History

Keep a list of recently used colors that the user can click to reuse.

5. Random Color Generator

Add a “Random” button that generates a random valid hex color.

6. HSL Mode

Add an alternative input mode using HSL (Hue, Saturation, Lightness) values.

7. Accessibility Checker

Calculate the contrast ratio between the chosen color and white/black text, showing WCAG compliance level.


The “Aha!” Moment

When you complete this project, you’ll have internalized something that many developers never truly understand: hexadecimal isn’t mysterious, it’s just a convenient way to write bytes.

Every time you see a hex color code, you’ll automatically decompose it:

  • First pair = Red intensity
  • Second pair = Green intensity
  • Third pair = Blue intensity

And you’ll know that:

  • #000000 is black because all channels are zero (no light)
  • #FFFFFF is white because all channels are maxed (full light)
  • #FF0000, #00FF00, #0000FF are pure red, green, blue
  • Equal R, G, B values produce grays (#888888, #AAAAAA, etc.)

This understanding transfers directly to:

  • CSS styling
  • Image manipulation libraries
  • Graphics programming
  • Understanding how digital images work
  • Debugging color-related issues

You’ve not just built a tool. You’ve rewired your brain to see hex colors intuitively.


Project Guide Version 1.0 - December 2025