One hex digit is four bits, and that is the whole reason
Sixteen is two to the fourth, so a single hex digit encodes exactly four bits — and two digits encode exactly one byte, the unit that matters everywhere in computing.
| Hex | Binary | Hex | Binary | Hex | Binary | Hex | Binary |
|---|---|---|---|---|---|---|---|
| 0 | 0000 | 4 | 0100 | 8 | 1000 | C | 1100 |
| 1 | 0001 | 5 | 0101 | 9 | 1001 | D | 1101 |
| 2 | 0010 | 6 | 0110 | A | 1010 | E | 1110 |
| 3 | 0011 | 7 | 0111 | B | 1011 | F | 1111 |
Memorise hex-to-binary in one direction only. The other direction is just reading the bits in groups of four, which is the reason hex exists: a 32-bit value is 8 hex digits instead of 32 characters, and a single typo in a byte is visible at a glance.
Why colour codes are hex
An RGB colour has three components, and each fits in one byte: red, green and blue from 0 to 255. Written as bytes that is six hex digits, and CSS reads them in that order.
#1D4ED8 is R = 0x1D = 29, G = 0x4E = 78, B = 0xD8 = 216, so rgb(29, 78, 216). Those are the exact values from this site's own palette. The shorthand #1D4 form is the same colour with each digit doubled — 0x1 → 0x11, 0xD → 0xDD — which is why #abc expands to #aabbcc and not to something else.
Adding a fourth byte gives alpha, so #1D4ED8FF is fully opaque and #1D4ED800 fully transparent. This 3-byte-plus-optional-alpha structure is why hex is the natural colour notation, and it is the same reason a memory dump is naturally grouped in pairs.
Masks, and why 0xFF is everywhere
A mask selects which bits you care about. Because a byte is two hex digits, 0xFF is the all-ones byte, and x & 0xFF keeps the low byte and discards everything above it. That single expression is the foundation of every binary protocol parser: it is how you read one byte out of a buffer.
- 0xFF — the low byte
- 0x0F — the low nibble, one hex digit
- 0x7F — clears the sign bit of a byte; the usual absolute value trick
- 0xFFFF / 0xFFFFFFFF — 16-bit and 32-bit all-ones
Where hex goes wrong
- Signed versus unsigned. 0xFF is 255 unsigned and −1 signed. The bits are identical; only the type declaration differs. This produces a class of bug where a length byte reads as 255 in one place and −1 in another.
- JavaScript's number limit. Numbers are doubles, exact to 2^53. A 16-hex-digit value is 64 bits and cannot be held exactly —
parseInt('FFFFFFFFFFFFFFFF', 16)is not 18,446,744,073,709,551,615. Anything wider than 8 bytes needs BigInt or a string, not a number. - Leading zeros matter. 0x7 is 00000011 in a byte but 111 in binary. When reading a binary protocol, pad to the byte width first or the positions shift.
- Confusing a number with a string. JavaScript's
parseIntstops at the first invalid character and returns what it got, soparseInt('1D4ED8xyz', 16)succeeds with a partial value instead of failing.
Frequently asked questions
How do I convert hex to decimal?
Expand each digit to its 4-bit binary form and sum the powers of two, or divide by 16 repeatedly and read the remainders up. 0x1D4ED8 = 1×16^5 + 13×16^4 + 4×16^3 + 14×16^2 + 13×16 + 8 = 1,925,080.
What is the decimal value of 0xFF?
255 unsigned. In signed two's complement the same byte means -1, because inverting 00000001 and adding 1 produces 11111111. Which one applies depends on how the value is typed, not on the bits.