I’ve been playing with WebGL recently. During this time, I found that I needed a way to convert HEX values to RGB and learnt of an interesting way to do that, which I took some time to understand and thought I’d write it down.

Here is the function, written in Typescript:

const hexToRgb = (hex: Hex): Rgb => {
  const num = parseInt(hex, 16);
  const r = num >> 16;
  const g = (num >> 8) & 255;
  const b = num & 255;

  return [r, g, b];

hexToRgb('000000'); // [0, 0, 0]
hexToRgb('FFFFFF'); // [255, 255, 255]
hexToRgb('2EC4B6'); // [46, 196, 182]

Note that that this is very basic for demonstration purposes. There’s a bunch of things you can do to improve this like removing a prepended hash character, handle alpha channels, shorthand HEX etc.

Ok let’s run through this step by step.

Javascript Number Type

Heads up! It’s important to know that Javascript stores the number type as 64 bits. There’s some meta data in the first 12 bits (which I won’t get into). So this leaves 52 bits to actually store a value. So 8 in Javascript is stored like this:


For the sake of this article, I will not be printing all these 0s and will be keeping it to the nearest byte or whatever makes sense.

Number Parsing

const num = parseInt(hex, 16);

parseInt converts a string to a number. It is pretty commonly used in Javascript due to the dynamic typing of the language. Why would we want that? Well when we want to perform mathematical operations, we need the right type to do so. It’s usually done like this: parseInt(value, 10).

But hold on, we’re passing in 16 here, not 10! This number represents the base.

Decimal, what us humans use day to day, is base 10. But there are other ways to represent numbers! Such as bits (base 2), and hex (base 16).

There’s a lot to learn here, but as an example, the number 26 is represented as:

It’s important to note that the value, when printed, will be base 10. This number won’t make much sense in this case and we don’t really care too much, because we will be performing bit operations.

Getting the Red Channel

const r = num >> 16;

The bitwise operator (>>/<<) shifts the bits of a number in a certain direction.

The number 8 is 00001000 in bits. So 8 >> 2 is equal to 00000010, which in base 10 is 2!

So why are we shifting by 16? Well each color channel takes up 8 bits of space. If our hex value is 2EC4B6, in base 2 it would look like this: 001011101100010010110110. Which is split up by the color channels: 00101110 (red) + 11000100 (green) + 10110110 (blue).

Due to this, our red and blue channels are way way way beyond the 255 color space! So by doing this shift, we bring the red channel to be in the first byte, which is what we want. The previous bits for the red and blue channels are now 0:

001011101100010010110110 >> 16; // 000000000000000000101110

Getting the Green Channel

const g = (num >> 8) & 255;

Well we now know why the bit shift is taking place, so it’s clear from the above that we only need to shift the values a single byte so the green channel can be the first byte, so let’s shift that:

001011101100010010110110 >> 8; // 000000000010111011000100

But hold on, this number is still way too large! That’s because we removed the green channel, but of course the red was shifted too and is still in memory.

Introducing the bitwise & operator.

The & operator combines the bit values of 2 numbers. For each bit, it returns 1 if both bit values equal 1, else it will be 0.

So 3 & 2 is 2. Because only the 2nd bit matches:

3: 011
2: 010
=  010

So how does that help us? As I said, if either bit does not equal 1, it is set to 0. So, by using the expression num & 255, we limit the value to only ever represent 8 bits. This is because 255 is the highest number possible in a single byte and looks like this 11111111.

num: 0010111011000100
255: 0000000011111111
=    0000000011000100

Bye bye red channel!

Getting the Blue Channel

const b = num & 255;

You’ll be pleased to know that blue is the easiest to get. We don’t have to do any bit shifting as it’s the first channel. But we do need to remove the 2 other channels. So we can just use the same expression num & 255 operator here to remove them.

num: 001011101100010010110110
255: 000000000000000011111111
=    000000000000000010110110


return [r, g, b];

We now have each channel stored as seperate variables as opposed to a single value. We can now return this however we like, as an array, an object or whatever. :)