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.