RGB to HEX converter

Type or paste channel values and get the hex, HSL and CSS forms, plus what those channels score against black and white. The arithmetic runs on your own machine, so no value you type is uploaded.

#663399

  • HEX #663399
  • RGB rgb(102, 51, 153)
  • HSL hsl(270, 50%, 40%)
  • CSS #663399

What these channels score for contrast

  • 2.50 to 1

    Black text on this colour

    AA ✗AAA ✗AA large ✗

  • 8.41 to 1

    White text on this colour

    AA ✓AAA ✓AA large ✓

White text is the stronger pairing on this colour.

When you need the channels, not the string

rgb(102, 51, 153) and #663399 are the same colour. The difference is that one of them is a value you can do arithmetic on. You want the numbers whenever a colour is an input rather than a label — and when the colour arrived as a hex string from a design file in the first place, HEX to RGB is this same converter pointed the other way, with the case for why the handover takes that form.

  • Canvas and WebGL. fillStyle takes a string, but pixel work does not: ImageData is a flat array of channel bytes, and anything that tints, blends or samples operates on those.
  • Interpolation. Gradients, chart scales and colour ramps mix channel by channel, so they need the endpoints as numbers.
  • Contrast. WCAG relative luminance is defined on linearised channel values. There is no way to compute a ratio from a hex string without splitting it first, which is what the panel above is doing.
  • Anything generated. Tint and shade ladders, theme builders, palette generators — all of them start from three numbers.

The custom property trick worth knowing

This is why a lot of design systems store colours as bare channels rather than as hex. Set --brand: 102 51 153, then write background: rgb(var(--brand)) for the solid colour and background: rgb(var(--brand) / 12%) for a tint of it. One token serves the colour and every transparency built from it.

The version people try first does not work: if --brand holds rgb(102, 51, 153), then rgba(var(--brand), .12) expands to rgba(rgb(102, 51, 153), .12), which is invalid and silently drops the declaration. Store the three numbers, not the function call. The RGB row above gives them to you with commas — delete those for the custom property form.

Old syntax and new

Modern CSS takes rgb(102 51 153 / 40%), with spaces between channels and a slash before alpha. rgba() is now simply an alias for rgb(), kept for compatibility, and current browsers accept either separator style in either function. Percentages work as well, and values outside 0 to 255 are clamped rather than rejected, which is why 300 becomes 255 here too instead of throwing an error at you.

Two things that catch people out

Round-tripping through HSL is lossy. hsl() lands on integer degrees and percentages, so #2F80ED converted to HSL and back comes out as #3182ED. Visually identical, not byte identical, and a diff or a visual regression test will flag it. Convert once from the source value rather than through a chain of formats.

Averaging channels gives muddy midpoints. A blue-to-yellow gradient interpolated in sRGB passes through grey, because the two colours cancel channel by channel. When the midpoint matters, let CSS mix somewhere better — color-mix(in oklab, …), or a gradient declared in oklch — and keep straight channel maths for the jobs that are genuinely arithmetic.

Designing a site? Siter.io builds it from Figma.

Hey there 👋  Friends from designmodo are here to help!