Color conversion

Batch HEX to RGB in JavaScript: Validation, Shorthand, and Alpha

Build a small HEX parser that handles three-, four-, six-, and eight-digit values, preserves invalid rows, and returns CSS-ready RGB or RGBA output.

Define the accepted input before converting

A one-off color can be pasted into a converter. A design-token export needs a repeatable rule for every row, including invalid values. This parser accepts a string containing an optional # followed by exactly 3, 4, 6, or 8 hexadecimal digits. It trims surrounding whitespace and expands shorthand before splitting channels.

For alpha colors, CSS places alpha last: #RGBA expands to #RRGGBBAA. It is not the #AARRGGBB convention used by some other platforms. The function rejects color names, rgb(...) strings, empty values, and non-string inputs instead of silently guessing their meaning.

Understand the channel arithmetic and alpha precision

Each full RGB channel contains two hexadecimal digits. The first digit represents multiples of sixteen and the second represents units: 3B is 3 × 16 + 11, or 59; 82 is 8 × 16 + 2, or 130; F6 is 15 × 16 + 6, or 246. This explains why #3B82F6 produces rgb(59, 130, 246) without changing the underlying color.

Shorthand duplicates each digit rather than padding the whole value with zeroes. For #0af, the expanded value is #00aaff. A single hexadecimal digit d therefore becomes 17 × d in an eight-bit channel. The same expansion applies to alpha: #0008 becomes #00000088, whose opacity is 136 / 255, approximately 0.5333.

Eight-bit alpha has 256 possible values, so 80 in the alpha position is 128 / 255, approximately 0.50196, rather than exactly one half. Keep the returned a value for calculations. The generated CSS rounds it to four decimal places for readability; that text is an approximation and should not replace the original value in a pipeline that requires exact byte preservation.

Parse channels and preserve alpha

Validation must happen before parseInt. For example, parseInt('ffzz00', 16) can return a partial number instead of reporting that the whole string is invalid. A full-string expression avoids accepting a corrupted token.

RGB channels stay in the 0–255 range. Alpha is the final byte divided by 255; keep that numeric result for calculations and round only the CSS output. The function returns null for an invalid input so a batch process can preserve the row and report it.

JavaScript
function hexToRgb(input) {
  if (typeof input !== 'string') return null;
  let hex = input.trim().replace(/^#/, '');
  if (!/^(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex)) {
    return null;
  }
  if (hex.length === 3 || hex.length === 4) {
    hex = [...hex].map((digit) => digit + digit).join('');
  }
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  const hasAlpha = hex.length === 8;
  const a = hasAlpha ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
  const alphaText = Number(a.toFixed(4));
  const css = hasAlpha
    ? 'rgba(' + [r, g, b, alphaText].join(', ') + ')'
    : 'rgb(' + [r, g, b].join(', ') + ')';
  return { r, g, b, a, css };
}

Keep errors next to their source values

Map each input to a result object rather than filtering invalid colors out of the list. This preserves row order, makes review easier, and prevents a bad color from shifting later values into the wrong design-token slot.

JavaScript
const inputs = ['#3b82f6', '#0af', '#0008', '#ff000080', 'oops'];
const results = inputs.map((input) => {
  const color = hexToRgb(input);
  return color
    ? { input, ok: true, css: color.css }
    : { input, ok: false, error: 'Invalid HEX color' };
});
console.table(results);
Expected output from the batch example
InputResult
#3b82f6rgb(59, 130, 246)
#0afrgb(0, 170, 255)
#0008rgba(0, 0, 0, 0.5333)
#ff000080rgba(255, 0, 0, 0.502)
oopsInvalid HEX color

Review boundary cases before importing a token file

Check valid inputs at both ends of each channel range: #000 should produce zeroes, #fff should produce 255s, and mixed-case input such as aBc should produce 170, 187, and 204. Confirm that surrounding whitespace is accepted deliberately. An internal space, a second #, a five-digit value, or a non-hexadecimal character must be rejected rather than repaired implicitly.

Check transparency separately from black and white. #ff000000 is a valid transparent red, not an invalid value and not an opaque red. #0000 is valid transparent black. A truthiness check on alpha would mishandle zero, so the implementation determines whether alpha exists from the expanded string length. It also checks the input type before calling trim, avoiding exceptions for null, numbers, and missing values.

If you compare a round trip, compare normalized channel values and the original alpha byte rather than raw strings. #fff, #FFFFFF, and FFFFFF are accepted spellings of the same opaque color in this parser, while its rounded rgba output is not intended as a lossless storage format. Keep the original input alongside the normalized result when traceability matters.

Integrate conversion without losing the data contract

The map example operates on an array that is already structured. Reading CSV, JSON, or a token package is a separate step: use a parser for that format and retain the token identifier, source row, and original value. Do not split an entire CSV file on commas, because quoted fields can contain commas and line breaks unrelated to color syntax.

Decide whether invalid rows should block the export or be returned to an editor for correction. A design-token build usually benefits from reporting every invalid token together and withholding the generated artifact until they are resolved. Avoid replacing a failed parse with black or dropping the row; either choice can turn an import problem into an unnoticed interface change.

For an existing array, conversion work grows with the number of rows. The example retains both the inputs and results in memory, which is convenient for small exports. Large imports may need chunking or a streaming format reader. Keep parsing independent of DOM updates so the same conversion rule can run in a build script, worker, or browser interface without acquiring different validation behavior.

Separate format conversion from visual checks

Converting an opaque sRGB HEX value to RGB changes its notation, not its color. This function does not convert Display P3, apply an ICC profile, or turn a screen color into a print-ready CMYK specification.

An alpha value is not enough to determine contrast: an rgba(...) foreground must be composited over the actual background first. Do not drop alpha and treat the remaining RGB channels as the visible result. Verify the rendered combination in the interface.

Use the HEX to RGB tool below for a quick opaque-color spot check. The parser in this article additionally handles alpha-bearing HEX values; the single-purpose tool is intended for solid #RGB and #RRGGBB inputs.

Sources & further reading

← Back to all articles