Hex to RGB in JavaScript

hex.slice(1).replace(/^(.)(.)(.)(.)?$/, '$1$1$2$2$3$3$4$4').match(/../g).map((h) => parseInt(h, 16))

The one-liner above turns '#ff5733' into [255, 87, 51]. slice(1) drops the #, the replace doubles each digit of a 3- or 4-digit code (f53 becomes ff5533), match(/../g) splits the digits into pairs, and parseInt(pair, 16) reads each pair in base 16: ff is 255, 57 is 87, 33 is 51.

const toRgb = (hex) => hex.slice(1).replace(/^(.)(.)(.)(.)?$/, '$1$1$2$2$3$3$4$4').match(/../g).map((h) => parseInt(h, 16));

toRgb('#ff5733');    // [255, 87, 51]
toRgb('#f53');       // [255, 85, 51]
toRgb('#ff573380');  // [255, 87, 51, 128]
toRgb('#f538');      // [255, 85, 51, 136]

The tool below shows #ff5733 as rgb(255 87 51). A fourth pair is alpha, as CSS Color 4 defines it, from 0 to 255: divide by 255 for the 0 to 1 that CSS writes, which is why the tool reads #ff573380 as rgb(255 87 51 / 0.502).

Validate, because parseInt doesn’t

parseInt stops at the first character that isn’t a digit in its base and returns what it read so far: parseInt('5g', 16) is 5, so '#ff5g33' comes out [255, 5, 51], no error. Check the input first:

function hexToRgb(hex) {
  let h = hex.replace(/^#/, '');
  if (!/^(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(h)) throw new Error(`Not a hex color: ${hex}`);
  if (h.length <= 4) h = [...h].map((c) => c + c).join('');
  const [r, g, b, a = 255] = h.match(/../g).map((pair) => parseInt(pair, 16));
  return { r, g, b, a: +(a / 255).toFixed(3) };
}

hexToRgb('#ff5733');    // { r: 255, g: 87, b: 51, a: 1 }
hexToRgb('#ff573380');  // { r: 255, g: 87, b: 51, a: 0.502 }
hexToRgb('#ff573');     // Error: Not a hex color: #ff573

The tool rejects five digits too: A hex color has 3, 4, 6 or 8 digits: "#ff573" has 5.

Three snippets that go wrong

['ff', '57', '33'].map(parseInt);          // [NaN, NaN, NaN]
'#f53'.match(/[0-9a-f]{2}/gi);              // ['f5']

const n = parseInt('ff573380', 16);
[n >> 24, (n >> 16) & 255, (n >> 8) & 255, n & 255];   // [-1, 87, 51, 128]
n >>> 24;                                               // 255

map passes each element’s index as parseInt’s second argument, so the radix becomes 0, 1 and 2. Matching pairs without expanding first reads #f53 as one channel. And the bit-shift version, fine for six digits, breaks on eight: >> works on signed 32-bit integers, 0xff573380 is above 2³¹, and the red channel comes out -1. >>> shifts unsigned.

Back to a CSS string or hex

const [r, g, b] = [255, 87, 51];
`rgb(${r} ${g} ${b})`;                                                  // 'rgb(255 87 51)'
'#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');  // '#ff5733'

In a browser you often needn’t parse anything: getComputedStyle(el).color for an element styled #ff5733 returns 'rgb(255, 87, 51)', and for #ff573380 it returns 'rgba(255, 87, 51, 0.5)', in Chromium 153 and WebKit 26.6 alike.

Try it in the Color Converter & Contrast Checker

Every format

FormatValueCopy
HEX#ff5733
RGBrgb(255 87 51)
HSLhsl(10.6 100% 60%)
OKLCHoklch(68.04% 0.21 33.69)

Contrast

The quick brown fox jumps over the lazy dog.

3.15:1

  • FailAA, normal text needs 4.5:1
  • PassAA, large text needs 3:1
  • FailAAA, normal text needs 7:1
  • FailAAA, large text needs 4.5:1

Runs in your browser. Nothing you enter is sent anywhere.