Convert Colors in JavaScript — HEX, RGB, and HSL with Working Code

Sooner or later every front-end project needs to move a color between formats: hex from a design token to RGB for canvas work, a computed rgb() string back to hex for a color picker, RGB to HSL to derive a hover shade. The math is small and worth owning instead of importing a library. This is a cookbook — every function below is complete and copy-paste ready.

HEX → RGB: Regex + parseInt(…, 16)

A hex color is just three base-16 numbers. Validate with a regex, expand 3-digit shorthand, then parse each pair:

function hexToRgb(hex) {
  let h = hex.replace(/^#/, '');
  if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(h)) return null;
  if (h.length === 3) {
    h = h.split('').map((c) => c + c).join(''); // f0c → ff00cc
  }
  return {
    r: parseInt(h.slice(0, 2), 16),
    g: parseInt(h.slice(2, 4), 16),
    b: parseInt(h.slice(4, 6), 16),
  };
}

hexToRgb('#1e90ff'); // { r: 30, g: 144, b: 255 }
hexToRgb('#f0c');    // { r: 255, g: 0, b: 204 }

For stricter input validation patterns (including alpha forms), see regex for hex color validation.

RGB → HEX: toString(16), Padded

The reverse uses Number.prototype.toString with radix 16. The one classic bug: values below 16 produce a single digit ("a" instead of "0a"), so pad to two characters:

function rgbToHex(r, g, b) {
  const toHex = (n) =>
    Math.max(0, Math.min(255, Math.round(n)))
      .toString(16)
      .padStart(2, '0');
  return '#' + toHex(r) + toHex(g) + toHex(b);
}

rgbToHex(30, 144, 255); // "#1e90ff"
rgbToHex(0, 10, 5);     // "#000a05"  ← padding matters!

RGB ↔ HSL: The Full Functions

RGB→HSL: lightness is the average of the max and min channels; saturation is how far apart they are; hue depends on which channel dominates:

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;
  if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };

  const d = max - min;
  const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  let h;
  switch (max) {
    case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
    case g: h = (b - r) / d + 2; break;
    default: h = (r - g) / d + 4;
  }
  return {
    h: Math.round(h * 60),
    s: Math.round(s * 100),
    l: Math.round(l * 100),
  };
}

rgbToHsl(30, 144, 255); // { h: 210, s: 100, l: 56 }

HSL→RGB works through a helper that maps each channel from the hue:

function hslToRgb(h, s, l) {
  h = ((h % 360) + 360) % 360; // normalise
  s /= 100; l /= 100;
  if (s === 0) {
    const v = Math.round(l * 255);
    return { r: v, g: v, b: v }; // achromatic gray
  }
  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  const p = 2 * l - q;
  const hue2rgb = (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;
  };
  return {
    r: Math.round(hue2rgb(h / 360 + 1 / 3) * 255),
    g: Math.round(hue2rgb(h / 360) * 255),
    b: Math.round(hue2rgb(h / 360 - 1 / 3) * 255),
  };
}

hslToRgb(210, 100, 56); // { r: 31, g: 143, b: 255 } (≈ #1e90ff)

Note the ±1 wobble on round-trips (30 became 31) — rounding to integer HSL loses a little precision. Harmless for UI work; keep floats if you need exactness.

Gotcha: getComputedStyle Returns rgb()

However you wrote the color in CSS — hex, hsl(), a keyword — the browser hands back a normalised rgb()/rgba() string. Parse it before converting:

const el = document.querySelector('.button');
getComputedStyle(el).backgroundColor;
// "rgb(30, 144, 255)" — even if CSS said #1e90ff or hsl(...)

function parseRgbString(str) {
  const m = str.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
  return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
}

rgbToHex(...Object.values(parseRgbString('rgb(30, 144, 255)')));
// "#1e90ff"

Reading Pixels from a Canvas

To sample a color from an image (an eyedropper feature, dominant-color extraction), draw it to a canvas and read the pixel data:

function getPixelColor(img, x, y) {
  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  const [r, g, b, a] = ctx.getImageData(x, y, 1, 1).data;
  return { r, g, b, a: a / 255 };
}

Two caveats: cross-origin images taint the canvas and make getImageData throw (the image must be same-origin or served with CORS headers and loaded with img.crossOrigin = "anonymous"); and on high-DPI displays remember that canvas coordinates are in canvas pixels, not CSS pixels.

The Copy-Paste Utility Module

Everything above, assembled into one module with the convenience combos:

// color-utils.js
export { hexToRgb, rgbToHex, rgbToHsl, hslToRgb, parseRgbString };

export function hexToHsl(hex) {
  const rgb = hexToRgb(hex);
  return rgb ? rgbToHsl(rgb.r, rgb.g, rgb.b) : null;
}

export function hslToHex(h, s, l) {
  const { r, g, b } = hslToRgb(h, s, l);
  return rgbToHex(r, g, b);
}

// derive a shade: darken('#1e90ff', 10) → hex 10% darker
export function darken(hex, amount) {
  const { h, s, l } = hexToHsl(hex);
  return hslToHex(h, s, Math.max(0, l - amount));
}

darken('#1e90ff', 10); // "#0077e6"-ish — same hue, deeper

With hexToHsl and hslToHex in hand you can generate entire tint/shade scales from one brand color — exactly the technique used in choosing a color palette.

Frequently Asked Questions

How do I convert a hex color to RGB in JavaScript?

Strip the #, split the string into two-character pairs, and parse each with parseInt(pair, 16). For example, "1e" becomes 30. Expand 3-digit shorthand like #f0c by doubling each digit first.

Why does getComputedStyle return rgb() instead of my hex value?

Browsers normalise computed colors to rgb() or rgba() regardless of how they were written in CSS. If you need hex for display or storage, parse the rgb() string with a regex and convert the channels using toString(16).

How do I read the color of a pixel in JavaScript?

Draw the source (image or drawing) to a canvas, then call ctx.getImageData(x, y, 1, 1).data, which returns [r, g, b, a] for that pixel. Cross-origin images must be CORS-enabled or the canvas becomes tainted and getImageData throws.

Try the Free Color Converter

Check your functions' output — convert HEX ↔ RGB ↔ HSL instantly in your browser. No signup, no cost.

Related articles