Skip to content
AldeaCode Logo
Color Converter / Hex to RGB Developer 100% local

Hex to RGB converter: the math, the gotchas, and the snippet

Hex and RGB are two notations for the same triplet of bytes. Converting between them is one base change and a regex, but every team manages to add a bug somewhere along the way. Here is the math, the corners that bite, and a snippet that survives the corners.

The math under hex

A hex colour like #ff8800 is three pairs of hexadecimal digits. Each pair is one byte, and each byte is an integer from 0 to 255. ff is 255, 88 is 136, 00 is 0. Read the pairs left to right and you get red, green, blue.

That is the whole conversion. #ff8800 becomes rgb(255, 136, 0). There is no rounding, no gamma curve, no colour space surprise. The two notations are byte for byte equivalent.

The shorthand #fff is the same logic with a single digit per channel, doubled to fill the byte. #fff is #ffffff, #abc is #aabbcc. The shorthand is convenient for round numbers and useless for anything tuned. A designer who sends you #a3c either means #aa33cc exactly or has not finished tuning the colour. Ask before you assume.

Alpha is the fourth pair

Modern browsers accept eight digit hex with alpha as the fourth pair. #ff880080 is the same orange at 50 percent opacity (80 is 128, half of 256). The corresponding CSS is rgba(255, 136, 0, 0.5) or rgb(255 136 0 / 0.5) in the new space separated syntax.

The alpha pair confuses because it is a 0 to 255 byte in hex but a 0 to 1 float in the rgba() call. 80 is not 0.8, it is 0.502. If you write rgba(255, 136, 0, 0.8) and expect it to match #ff8800cc you will be three percent off and never see it.

Four digit shorthand exists too. #f80c expands to #ff8800cc. Same shortcut as the three digit form, same warning about precision.

Doing it in code

The JavaScript pattern is to strip the leading hash, expand a three or four digit shorthand, and parse two characters at a time with parseInt(pair, 16). The regex /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i captures the three pairs in one match. Anything else (extra spaces, missing hash with a regex that requires it, eight digit string passed to a six digit regex) returns null and the rest of the function returns NaN. That is the bug pipeline.

Two failure modes show up in production. The first is forgetting to handle the optional hash, so ff8800 and #ff8800 go down different paths. The second is passing #abc to a regex that demands six digits and silently producing undefined. Use a permissive parser that accepts both lengths, both with and without the hash, and both upper and lower case. The standard library does not exist for this in the browser, so the snippet below is the canonical version.

When the visual converter beats the snippet

For a one off (a designer pastes #3a86ff into Slack and you need the rgba for a CSS shadow) opening a console and typing parseInt('3a', 16) three times is six steps too many. Paste it into a converter that runs in your browser, copy the output, move on. Do the snippet thing when the conversion lives in a build pipeline or a theme generator.

Working example

javascript
// Hex to RGB, handles 3, 4, 6 and 8 digit forms.
// Returns { r, g, b, a } with a between 0 and 1, or null on bad input.
function hexToRgb(hex) {
  if (typeof hex !== "string") return null;
  let h = hex.trim().replace(/^#/, "").toLowerCase();

  // Expand 3 or 4 digit shorthand to 6 or 8.
  if (h.length === 3 || h.length === 4) {
    h = h.split("").map((c) => c + c).join("");
  }

  if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(h)) return null;

  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;

  return { r, g, b, a: Math.round(a * 1000) / 1000 };
}

// hexToRgb("#ff8800")    -> { r: 255, g: 136, b: 0, a: 1 }
// hexToRgb("#ff880080")  -> { r: 255, g: 136, b: 0, a: 0.502 }
// hexToRgb("#abc")       -> { r: 170, g: 187, b: 204, a: 1 }
// hexToRgb("not a hex")  -> null

Just need the result?

Paste the hex into the colour converter on aldeacode.com and the RGB, the rgba with alpha, the HSL and the OKLCH equivalents land in one click. No console, no snippet, the math is correct on the first try and the page never sends a single byte to a server.

Open Color Converter (Hex / RGB / HSL) →

Frequently asked questions

Is #FFF the same as #FFFFFF?

Yes. Three digit shorthand doubles each character, so #fff expands to #ffffff and #abc to #aabbcc. The two render identically. The shorthand only works when each pair is a doubled digit, otherwise you have to use the full six.

How does eight digit hex alpha map to the rgba float?

Divide the alpha byte by 255, not by 256. So #ff880080 becomes rgba(255, 136, 0, 0.502). Common bug: people use 0.5 thinking 80 is exactly half, it is not.

Should I write the hash or not?

Always write it in CSS and HTML attributes. In code paths that accept hex strings, accept both. The hash is optional in most parsers but required in CSS, so normalising on the way in saves you a class of bugs.