Color Formats Explained: HEX, RGB, RGBA and CMYK
Learn how HEX, RGB, RGBA and CMYK work, convert between them by hand and in code, and check contrast for accessibility with verified examples.

You pick a blue in a design tool and it hands you #1e90ff. Your stylesheet wants rgb(30 144 255). The overlay needs the same blue at half opacity, so now you need rgba(30, 144, 255, 0.5) or perhaps #1e90ff80. The print shop asks for CMYK values, and the number they give back looks nothing like what you saw on screen. These are all the same colour described in different languages, and the translations are not always as simple as they look.
This guide is for front-end developers, designers who touch code, and anyone who has ever copied a colour value and wondered what the digits mean. You will learn how screens and printers make colour, how each notation works down to the byte, how to convert between them by hand and in code, why RGB to CMYK is an approximation rather than a formula, how to check contrast for accessibility, and how to keep colours consistent in a design system. Every code sample in this article was run, and the outputs shown are the real ones.
Additive and subtractive colour: two ways to make a colour
Colour formats make sense once you know what the medium is doing with light.
A screen is additive. It starts dark and emits light. Each pixel contains tiny red, green and blue sub-pixels, and the colour you see is the sum of the light they emit. Turn all three off and you get black. Turn all three to full and you get white. Red plus green light looks yellow, which surprises anyone raised on paint mixing.
Paper is subtractive. It starts white, reflecting all the light that hits it, and ink removes some of that light. Cyan ink absorbs red, magenta ink absorbs green and yellow ink absorbs blue. Layer all three and, in theory, you absorb almost everything and get a dark brown. In practice the result is muddy, which is why printers add a fourth, black ink. That ink is called the key plate, and it is where the K in CMYK comes from.
This difference is the root of nearly every conversion problem in this article. RGB describes how much light to emit. CMYK describes how much ink to lay down. They are different physical processes with different ranges of reachable colours, so converting between them is a translation, not an arithmetic identity.
| Property | RGB (screens) | CMYK (print) |
|---|---|---|
| Model | Additive, light emitted | Subtractive, light absorbed |
| Starting point | Black screen | White paper |
| Primaries | Red, green, blue | Cyan, magenta, yellow, plus black |
| Full mix | White | Dark, muddy brown without black ink |
| Typical use | Web, apps, video, photography | Brochures, packaging, books |
| Channel range | 0 to 255 in the common 8-bit case | 0 to 100 percent per ink |
What the formats are, and a short history
The RGB colour model rests on the observation that human colour vision uses three kinds of cone cells, so mixing three well-chosen lights can reproduce a wide range of perceived colours. The idea of building colour from red, green and blue light is old, and it became practical with colour television and then computer displays. The RGB colour model article on Wikipedia gives a thorough overview of the theory and the devices built on it.
The web needed a way to write these values in text. Early HTML allowed colour names and a hexadecimal notation, where each of the three channels is written as two hexadecimal digits. CSS then added the functional rgb() notation, later rgba() for transparency, then hsl() and hsla(). Over time, the CSS Color specifications grew to add more colour spaces and a unified syntax. The current reference for what browsers accept is the CSS color value page on MDN.
For web colours, the defining standard for a long time has been sRGB, a colour space created in the 1990s to describe typical monitors, so that an RGB triplet means roughly the same thing on different screens. When CSS says rgb(30 144 255) with no other qualification, it means that triplet in sRGB. We will come back to why that matters when we get to wide-gamut displays.
CMYK has a different lineage. It comes from the printing trade, where colour is built from halftone dots of cyan, magenta, yellow and black ink. The CMYK colour model article explains the inks, the role of the key plate and why the model exists. Unlike sRGB, there is no single CMYK. The result depends on the inks, the paper and the press, so each printing condition has its own characterisation. That detail is the heart of the RGB to CMYK problem.
The RGB model and 8-bit channels
In the format used by most web work, each pixel has three channels, and each channel is stored in eight bits. Eight bits give 256 possible values, from 0 to 255. Zero means none of that light, and 255 means full intensity.
Three channels of 256 levels give 256 × 256 × 256 = 16,777,216 distinct triplets. That is the origin of the phrase "16.7 million colours". It is a count of codes, not a count of colours the human eye can tell apart, and not a count of colours the real world contains.
Some useful reference points:
| Colour | RGB triplet | HEX |
|---|---|---|
| Black | 0, 0, 0 | #000000 |
| White | 255, 255, 255 | #ffffff |
| Pure red | 255, 0, 0 | #ff0000 |
| Pure green | 0, 255, 0 | #00ff00 |
| Pure blue | 0, 0, 255 | #0000ff |
| Yellow (red plus green) | 255, 255, 0 | #ffff00 |
| Mid grey | 128, 128, 128 | #808080 |
| Dodger blue | 30, 144, 255 | #1e90ff |
Equal channel values produce a neutral grey, because no colour dominates. Raising one channel relative to the others shifts the hue towards that primary.
One more detail that matters later: the numbers are not proportional to physical light intensity. sRGB applies a curve, called gamma encoding, so that the 256 levels are spread more evenly across what human vision perceives. A value of 128 is not half as much light as 255. It is considerably less than half. This is why the contrast ratio formula we cover later has to undo the curve first.
HEX notation, digit by digit
A hex colour is a hash sign followed by hexadecimal digits. Hexadecimal is base 16, with digits 0 to 9 and then a to f for the values ten to fifteen. Case does not matter, so #FF6347 and #ff6347 are the same colour.
Two hex digits can express 16 × 16 = 256 values, which is exactly one 8-bit channel. So a six-digit colour is three channel bytes written in a row: two digits for red, two for green, two for blue.
The four lengths
CSS accepts four lengths of hex colour. All four are supported in current mainstream browsers, though the 4 and 8-digit forms arrived later than the 3 and 6-digit forms, so check your support targets if you maintain very old browsers.
| Form | Example | Meaning |
|---|---|---|
| 3 digits | #f60 |
Shorthand for #ff6600. Each digit is doubled. |
| 4 digits | #f608 |
Shorthand for #ff660088. Each digit is doubled, alpha last. |
| 6 digits | #ff6600 |
Red, green, blue, two digits each. |
| 8 digits | #ff660080 |
Red, green, blue, then alpha. |
The shorthand is not a rounding of the long form. It is a strict duplication of each digit. #f60 means #ff6600 and cannot express #fe6600. Only colours whose channel digits are paired, such as ff, 66 and 00, have a short form.
Any other length is invalid. A five-digit or seven-digit value is silently dropped by the browser, which usually means the colour declaration is ignored and the element falls back to an inherited or default colour. That silent failure is one of the most common sources of "my colour is not applying" bugs.
Converting HEX to RGB by hand
Take #1e90ff. Split it into three pairs: 1e, 90, ff.
To convert a pair, multiply the first digit by 16 and add the second. Remember that a is 10, b is 11, c is 12, d is 13, e is 14 and f is 15.
1e: 1 × 16 + 14 = 3090: 9 × 16 + 0 = 144ff: 15 × 16 + 15 = 255
So #1e90ff is rgb(30, 144, 255).
A second example, #ff6347:
ff: 25563: 6 × 16 + 3 = 9947: 4 × 16 + 7 = 71
That gives rgb(255, 99, 71), a tomato red.
Converting RGB to HEX by hand
Reverse the process. Divide each channel by 16. The whole-number part is the first digit and the remainder is the second.
For 144: 144 ÷ 16 = 9 remainder 0, so the pair is 90. For 30: 30 ÷ 16 = 1 remainder 14, and 14 is e, so the pair is 1e. For 255: 255 ÷ 16 = 15 remainder 15, giving ff.
Always pad to two digits. A channel value of 7 must become 07, not 7. Forgetting the leading zero shifts every following channel and produces a completely different colour, or an invalid length. For instance rgb(0, 128, 7) is #008007, and writing #08087 would be a five-digit value that the browser rejects.
Reading a hex colour at a glance
With practice you can estimate a colour from its hex digits. The first digit of each pair gives the rough level, since it is the sixteens place. #e0 is bright, #80 is middle and #10 is dark. A colour with equal pairs is grey. A colour with a large red pair and small green and blue pairs is reddish. This skill is not necessary, but it speeds up debugging.
rgb() and rgba() in CSS
The functional notation gives each channel as a number from 0 to 255, or as a percentage from 0 percent to 100 percent.
.a { color: rgb(30, 144, 255); }
.b { color: rgb(30 144 255); }
.c { color: rgb(11.8% 56.5% 100%); }
The first line is the traditional comma-separated syntax. The second is the modern space-separated syntax from the newer CSS Color specification. Both mean the same thing in current browsers. Mixing styles within one function, such as commas for some values and spaces for others, is invalid.
Adding opacity
The traditional syntax had two function names: rgb() for opaque colours and rgba() when you wanted an alpha value. In the modern syntax, the alpha comes after a slash, and rgb() accepts it directly:
.overlay-old { background: rgba(30, 144, 255, 0.5); }
.overlay-new { background: rgb(30 144 255 / 0.5); }
.overlay-pct { background: rgb(30 144 255 / 50%); }
All three produce the same translucent blue. In modern browsers, rgba() is just an alias of rgb(), and both syntaxes are accepted by either name. If you need to support very old browsers, the comma form with rgba() is the safest, and for current browsers either style works. Check the browser support tables on MDN for your own targets rather than assuming.
Alpha is a value from 0 to 1, or from 0 percent to 100 percent. Zero is fully transparent and one is fully opaque. Alpha is not a physical measurement. It tells the compositor how much of this colour to blend with whatever is behind it.
What alpha does, mathematically
For a foreground colour F with alpha a over a background B, the common blending rule is applied per channel:
result = a × F + (1 − a) × B
If you paint rgb(30 144 255 / 0.5) over white (255, 255, 255), the red channel becomes 0.5 × 30 + 0.5 × 255 = 142.5, the green becomes 0.5 × 144 + 0.5 × 255 = 199.5, and the blue stays 255. The browser rounds, so you get roughly rgb(143, 200, 255), a lighter blue. Over black the same layer gives roughly rgb(15, 72, 128), a darker blue. The same translucent colour therefore looks different on every background, which is why contrast checks must be done on the final blended colour, not on the raw alpha colour.
Note one practical catch. The opacity property and an alpha colour are different things. opacity: 0.5 on an element fades the entire element, including its text, borders and children. An alpha colour on background fades only the background. Mixing them up is a very common cause of washed-out text.
Alpha and 8-digit HEX: the ordering trap
Eight-digit hex appends the alpha as a fourth byte: #RRGGBBAA. The alpha byte runs from 00 (transparent) to ff (opaque). So #1e90ff80 is the dodger blue at about half opacity.
The convention is not universal across platforms. CSS and the web put alpha last. Some other systems, notably Android's colour integers and certain graphics APIs, put alpha first: #AARRGGBB. If you paste an Android-style value into CSS, the colours rotate: the alpha byte is read as red, and every other channel shifts one place. The result is a colour that is wrong but not obviously broken. Always check which order a source uses before pasting it.
Converting alpha to a byte and back
Alpha in CSS is a fraction from 0 to 1, while the hex form is an integer from 0 to 255. The conversion is:
- alpha byte = round(alpha × 255)
- alpha fraction = byte ÷ 255
Because of rounding, a round trip is not always exact. Half opacity, 0.5, becomes 0.5 × 255 = 127.5, which rounds to 128, which is hex 80. Convert 80 back and you get 128 ÷ 255, which is 0.502. Not 0.5. The difference is invisible to the eye, but it matters if you compare values in a test or in a design-token diff.
Here are a few worked conversions, produced by the code in the next section:
| Input | Alpha byte | Output |
|---|---|---|
rgba(30, 144, 255, 0.5) |
128, 80 |
#1e90ff80 |
rgba(255, 99, 71, 0.8) |
204, cc |
#ff6347cc |
rgba(0, 0, 0, 0.33) |
84, 54 |
#00000054 |
Going the other way, #1e90ff80 becomes rgba(30, 144, 255, 0.502) when rounded to three decimals, and #0f08 expands to #00ff0088 and becomes alpha 0.533.
The fact that 0.33 becomes 84 and returns as about 0.329 is worth remembering. If your design tokens store alpha as a clean two-decimal fraction, store that as the source of truth and generate hex from it, not the reverse.
Conversion functions you can run
The functions below handle the 3, 4, 6 and 8-digit hex forms, expand shorthand, validate input, and convert in both directions. They were run under Node.js and the outputs are shown after the code.
function hexToRgba(hex) {
let h = hex.replace(/^#/, "");
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
if (![6, 8].includes(h.length) || !/^[0-9a-f]+$/i.test(h)) {
throw new Error("Invalid hex: " + hex);
}
const n = (i) => parseInt(h.slice(i, i + 2), 16);
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? +(n(6) / 255).toFixed(3) : 1 };
}
const two = (n) => Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, "0");
function rgbaToHex(r, g, b, a = 1) {
return "#" + two(r) + two(g) + two(b) + (a < 1 ? two(a * 255) : "");
}
console.log(hexToRgba("#1e90ff"));
console.log(hexToRgba("#f00"));
console.log(hexToRgba("#1e90ff80"));
console.log(hexToRgba("#0f08"));
console.log(rgbaToHex(30, 144, 255));
console.log(rgbaToHex(30, 144, 255, 0.5));
console.log(rgbaToHex(255, 99, 71, 0.8));
The real output:
{ r: 30, g: 144, b: 255, a: 1 }
{ r: 255, g: 0, b: 0, a: 1 }
{ r: 30, g: 144, b: 255, a: 0.502 }
{ r: 0, g: 255, b: 0, a: 0.533 }
#1e90ff
#1e90ff80
#ff6347cc
Three details are worth pointing out. The two helper clamps to the 0 to 255 range before formatting, so a computed value of 256 or minus 3 never produces a three-character or negative result. It uses padStart so low values keep their leading zero. And the function omits alpha when it is 1, which keeps opaque colours in the short six-digit form.
The same logic in Python, run with Python 3:
def hex_to_rgba(h):
h = h.lstrip("#")
if len(h) in (3, 4):
h = "".join(c * 2 for c in h)
if len(h) not in (6, 8):
raise ValueError("bad hex")
v = [int(h[i:i+2], 16) for i in range(0, len(h), 2)]
a = round(v[3] / 255, 3) if len(v) == 4 else 1.0
return v[0], v[1], v[2], a
def rgba_to_hex(r, g, b, a=1.0):
s = "#%02x%02x%02x" % (round(r), round(g), round(b))
if a < 1:
s += "%02x" % round(a * 255)
return s
print(hex_to_rgba("#1e90ff"))
print(hex_to_rgba("#0f08"))
print(rgba_to_hex(30, 144, 255, 0.5))
(30, 144, 255, 1.0)
(0, 255, 0, 0.533)
#1e90ff80
A subtle language difference: Python's built-in round uses round-half-to-even, while JavaScript's Math.round rounds halves upward. For the single value 127.5 both give 128, because 128 is even. But for values that land exactly on a half, such as 0.5 × 255 variants or computed blends, the two can differ by one in some cases. When two systems must agree to the last digit, write down the rounding rule and test the half cases. A one-unit difference in a channel is invisible on screen, but it will fail an exact string comparison.
HSL, HWB and the newer CSS formats
RGB is convenient for hardware and awkward for people. It is hard to answer "make this blue a bit lighter" by editing three numbers. Other notations describe colour in terms closer to how designers think.
HSL
hsl() stands for hue, saturation and lightness. Hue is an angle on the colour wheel, from 0 to 360 degrees, where red is at 0, green near 120 and blue near 240. Saturation is the intensity of the colour in percent, and lightness runs from black at 0 percent through the pure colour at 50 percent to white at 100 percent.
.base { color: hsl(210 100% 56%); }
.lighter { color: hsl(210 100% 70%); }
.darker { color: hsl(210 100% 40%); }
Keeping hue and saturation fixed while varying lightness gives you a family of related shades, which is useful for hover states and tints. HSL is a rearrangement of the same sRGB values, so it reaches no new colours, and it has a known weakness: equal lightness numbers do not look equally light across hues. A yellow at 50 percent lightness looks far brighter than a blue at 50 percent lightness. So HSL is easy to reason about but unreliable for generating palettes with consistent perceived brightness.
HWB
hwb() stands for hue, whiteness and blackness. You choose a hue and then say how much white and how much black to mix in, which resembles how people think about mixing paint. It is supported in current browsers, but check the support table for your targets.
Lab, LCH, OKLab and OKLCH
The CSS Color Level 4 specification added colour spaces that aim to be perceptually uniform, meaning equal numeric steps correspond more closely to equal visual differences. lab() and lch() are based on the CIE colour model. oklab() and oklch() use a more recent space, designed to behave better when you change lightness or hue.
.accent { color: oklch(70% 0.15 250); }
The three values are lightness, chroma (colourfulness) and hue angle. The practical benefit is that you can build a scale of colours by changing only the lightness, and they will look evenly spaced, which is much harder to achieve in HSL.
Honest caveats: these formats are newer, browser support has been growing, and you should check the MDN compatibility data before depending on them in production. A standard defensive approach is to declare an sRGB fallback first and the modern value after it, since browsers ignore declarations they do not understand and use the last valid one:
.accent {
color: #3b82f6;
color: oklch(62% 0.19 260);
}
Also note that perceptual spaces can describe colours that sRGB cannot display. When a value falls outside what the device can show, the browser maps it into range, and the exact mapping is implementation behaviour you should test rather than assume.
The color() function and wide gamut
The color() function lets you name a colour space explicitly, such as display-p3:
.vivid { color: color(display-p3 0 1 0); }
Display P3 is a wider colour space than sRGB. Many modern phones, laptops and monitors can show colours outside sRGB, and these screens render color(display-p3 0 1 0) as a green more saturated than any sRGB green. On a screen limited to sRGB, the same value is mapped to the nearest displayable colour. This is where hex and rgb() fall short: they are sRGB only, so they cannot address the extra colours at all. If your brand colour is vivid enough to fall outside sRGB, the hex code is only an approximation of it.
You can test support in CSS itself with a feature query:
@media (color-gamut: p3) {
.vivid { color: color(display-p3 0 1 0); }
}
For most interfaces, sRGB is enough, and hex remains the best common denominator. Wide-gamut colour matters most for photography, video and brand colours that must look vivid on capable displays.
CMYK for print
CMYK describes four ink amounts, each from 0 to 100 percent. Cyan, magenta and yellow are the subtractive primaries, and K is black.
Why add black at all? Three reasons. Equal amounts of cyan, magenta and yellow inks in practice give a brownish dark colour, not a true black. Black text is the most common thing printed, and printing it with three overlaid inks would be expensive and would blur because the plates must register perfectly. And a dedicated black ink gives deeper shadows and lets the printer use less coloured ink overall.
Printing is not continuous. Presses lay down patterns of tiny dots, and your eye blends them. The percentage of a cyan value is roughly the fraction of the area covered with cyan dots, which is why CMYK values are percentages rather than bytes.
The naive RGB to CMYK formula
Many tools, tutorials and code snippets use a simple formula, and it helps to see exactly what it does and what it ignores. Treat the red, green and blue values as fractions R, G, B between 0 and 1. Then:
- K = 1 − max(R, G, B)
- C = (1 − R − K) ÷ (1 − K)
- M = (1 − G − K) ÷ (1 − K)
- Y = (1 − B − K) ÷ (1 − K)
If K is 1, the colour is pure black and C, M and Y are zero.
Here is the formula in JavaScript. Remember what this is: an approximation that is useful for rough previews and for understanding the model, and not a way to prepare artwork for a press.
function naiveCmyk(r, g, b) {
const [R, G, B] = [r, g, b].map((v) => v / 255);
const k = 1 - Math.max(R, G, B);
if (k === 1) return { c: 0, m: 0, y: 0, k: 100 };
const f = (v) => Math.round(((1 - v - k) / (1 - k)) * 100);
return { c: f(R), m: f(G), y: f(B), k: Math.round(k * 100) };
}
console.log(naiveCmyk(30, 144, 255));
console.log(naiveCmyk(255, 0, 0));
console.log(naiveCmyk(0, 0, 0));
console.log(naiveCmyk(128, 128, 128));
The real output:
{ c: 88, m: 44, y: 0, k: 0 }
{ c: 0, m: 100, y: 100, k: 0 }
{ c: 0, m: 0, y: 0, k: 100 }
{ c: 0, m: 0, y: 0, k: 50 }
And in Python:
def naive_cmyk(r, g, b):
r, g, b = r / 255, g / 255, b / 255
k = 1 - max(r, g, b)
if k == 1:
return 0, 0, 0, 100
return tuple(round((1 - x - k) / (1 - k) * 100) for x in (r, g, b)) + (round(k * 100),)
print(naive_cmyk(30, 144, 255))
print(naive_cmyk(255, 140, 0))
(88, 44, 0, 0)
(0, 45, 100, 0)
So by this formula, dodger blue #1e90ff is C88 M44 Y0 K0, and a vivid orange rgb(255, 140, 0) is C0 M45 Y100 K0. Notice that the pure RGB red comes out as M100 Y100, which is plausible, and mid-grey comes out as pure K50, which is also plausible. The formula is not nonsense. It is just blind to everything that makes real print complicated.
Why the formula is not enough
It ignores the ink. The formula assumes ideal inks that absorb exactly one primary each. Real inks are impure. A press using one set of inks on coated paper produces different colours from the same percentages on uncoated newsprint. The numbers C88 M44 Y0 K0 do not specify a colour on their own. They specify a recipe, and the colour depends on who cooks it.
It ignores the gamut. A gamut is the range of colours a device can reproduce. Screens can produce very saturated cyans, greens and blues that no CMYK press can reach. When a bright RGB colour lies outside the CMYK gamut, any conversion must pull it in to something reproducible. The naive formula cannot, because it knows nothing about the destination gamut. It outputs mathematically tidy numbers that the press will not honour in the way the screen suggested. Saturated blues and electric greens usually shift the most, and the printed result looks duller than the screen.
It has no black generation strategy. For a colour like dark teal, there are many valid CMYK recipes: lots of coloured ink with little black, or less coloured ink with more black. The naive formula always extracts the maximum possible black, which is one extreme. Real workflows choose how much black to generate, and how much total ink to allow, because too much ink on the page smears and dries slowly. Press operators work to a total ink limit, so a recipe that is fine on screen can break a print specification.
It is not colour managed. Professional conversion uses ICC profiles, which are files that describe how a particular device or printing condition maps device numbers to a device-independent colour. The workflow converts RGB into a profile connection space and then out to the CMYK profile of the target press, applying a rendering intent to decide how to handle out-of-gamut colours. The result depends on the profile, which is exactly the information the naive formula does not have.
So the honest summary is this. The simple formula is good for a rough number, and it is a fine teaching tool. Final print artwork should be converted in software that supports colour management with the profile your printer specifies, and checked against a printed proof. If your print provider gives you a profile name, use that profile.
sRGB, wide gamut and print
It is tempting to think wide-gamut screens help with print. In practice, a wide-gamut display may show colours that the press cannot reproduce at all, so a vivid display P3 green looks even further from its printed version. Soft-proofing, where software simulates the print profile on screen, is the usual way to preview what the press will really deliver.
The reverse direction is also lossy. Converting CMYK to RGB gives you a colour you can show, but you cannot recover the original ink choices, because many CMYK recipes map to the same RGB colour. A round trip RGB to CMYK to RGB therefore rarely returns the starting colour exactly.
Contrast and accessibility
Choosing a colour is not only about taste. People with low vision, colour vision deficiency, or just a phone in direct sunlight need to read your text. The Web Content Accessibility Guidelines (WCAG) define a measurable rule for text and background contrast.
The relative luminance and contrast ratio formulas
First, convert each 8-bit channel to a fraction, then undo the sRGB gamma curve to get a linear value. For a channel value c in the range 0 to 1:
- if c ≤ 0.04045, linear = c ÷ 12.92
- otherwise, linear = ((c + 0.055) ÷ 1.055) raised to the power 2.4
Relative luminance L combines the linear channels with weights that reflect how sensitive the eye is to each primary:
L = 0.2126 × R + 0.7152 × G + 0.0722 × B
Green dominates, red contributes a little and blue contributes least, which is why a pure blue looks so dark next to a pure green.
The contrast ratio between two colours with luminances L1 (the lighter) and L2 (the darker) is:
ratio = (L1 + 0.05) ÷ (L2 + 0.05)
The ratio runs from 1:1 (identical colours) to 21:1 (black on white). WCAG 2 sets minimums at the AA level of 4.5:1 for normal text and 3:1 for large text and for meaningful user-interface components, and a stricter AAA level of 7:1 for normal text. Check the current WCAG documents for the exact definitions of "large text" and the success criteria before you cite them in an audit.
A worked example
Compute the contrast of white text on dodger blue #1e90ff, which is rgb(30, 144, 255).
The linear channels, from running the code below:
- Red: 30 ÷ 255 = 0.1176, which gives a linear value of about 0.01298
- Green: 144 ÷ 255 = 0.5647, which gives a linear value of about 0.27889
- Blue: 255 ÷ 255 = 1, which gives 1
Luminance: 0.2126 × 0.01298 + 0.7152 × 0.27889 + 0.0722 × 1 = 0.00276 + 0.19946 + 0.0722, about 0.2744.
White has luminance 1. So the ratio is (1 + 0.05) ÷ (0.2744 + 0.05) = 1.05 ÷ 0.3244, about 3.24:1.
That fails the 4.5:1 minimum for normal text, and it passes only the 3:1 level for large text and interface components. White text on this blue looks fine to many designers on a good monitor and is still not accessible for body copy.
Here is the verified code:
const lin = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
const lum = ([r, g, b]) => 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
function contrast(a, b) {
const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
return (x + 0.05) / (y + 0.05);
}
console.log(contrast([255, 255, 255], [0, 0, 0]).toFixed(2));
console.log(contrast([255, 255, 255], [119, 119, 119]).toFixed(3));
console.log(contrast([255, 255, 255], [118, 118, 118]).toFixed(3));
console.log(contrast([255, 255, 255], [30, 144, 255]).toFixed(3));
console.log(contrast([0, 0, 0], [30, 144, 255]).toFixed(3));
The real output:
21.00
4.478
4.542
3.236
6.489
The same computation in Python gave 4.478 for white on #777777, 3.236 for white on #1e90ff, and 6.489 for black on #1e90ff.
Read these numbers carefully. Black on white is the maximum, 21. Mid-grey #777777 on white scores 4.478, just under 4.5, so it fails AA for normal text. Drop one step to #767676 and the ratio becomes 4.542, which passes. A single unit in each channel moves a colour across the line, so do not trust your eyes at the boundary. Compute it.
Also notice that black text on dodger blue scores 6.489, much higher than white text on the same blue. Intuition says white on blue, but the mathematics prefers black here, because this blue is fairly light in terms of luminance.
Accessibility pitfalls beyond the ratio
- Alpha colours. Compute contrast on the blended result against the real background, as shown earlier. A 50 percent black text on white is a mid-grey, and its ratio is much lower than the black the designer imagined.
- Do not rely on colour alone. Roughly speaking, a noticeable share of people have some form of colour vision deficiency, so never use red versus green as the only signal for an error or a status. Add an icon, a label or a pattern.
- States and borders. Focus rings, input borders and icons count as interface components and need sufficient contrast against their surroundings too.
- Dark mode. A palette that passes in light mode does not automatically pass in dark mode. Recheck both.
- Forced colours. Some operating systems provide high-contrast modes that override your colours. Test that your UI stays usable, and use system colour keywords where appropriate.
- The limits of WCAG 2 contrast. The formula is a simplification of human vision. It can over-rate some dark-on-dark combinations and under-rate others. Treat it as a minimum bar and a legal reference, and still test with real users and real devices.
Colour in design systems
When a team uses fifty hex codes scattered across a codebase, a rebrand becomes a search-and-replace nightmare. Design systems solve this with tokens: named values that carry meaning.
Primitive and semantic tokens
A common structure has two layers. Primitive tokens name the raw palette, such as blue-500. Semantic tokens name the role, such as color-action-primary or color-text-muted, and point at a primitive.
:root {
/* primitives */
--blue-500: #1e90ff;
--blue-700: #0b5cad;
--gray-900: #111827;
--white: #ffffff;
/* semantic */
--color-action: var(--blue-700);
--color-text: var(--gray-900);
--color-surface: var(--white);
}
.button {
background: var(--color-action);
color: var(--color-surface);
}
Components reference only semantic tokens. To retheme or to switch to dark mode, you change what the semantic tokens point to, and no component needs to change.
Custom properties and alpha
A frustration with CSS custom properties and hex colours is that you cannot easily apply alpha to a hex variable. One answer is to store the channels separately, in the space-separated form, and combine them where needed:
:root { --brand-rgb: 30 144 255; }
.overlay { background: rgb(var(--brand-rgb) / 0.5); }
.border { border-color: rgb(var(--brand-rgb) / 0.25); }
Newer CSS also offers relative colour syntax and the color-mix() function for deriving a colour from another, though you should check support before you rely on either. Storing the raw channels works in every browser that supports the modern syntax, and it keeps the conversion simple.
Dark mode
Prefer redefining semantic tokens under a media query rather than inverting colours:
:root {
--color-surface: #ffffff;
--color-text: #111827;
}
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #0f172a;
--color-text: #e5e7eb;
}
}
Pick the dark values deliberately and test their contrast. Pure white text on pure black can feel harsh and tends to produce halation for some readers, so many systems use slightly softened pairs, but this is a design judgement, not a rule.
Source of truth and format
Choose one canonical format for your tokens and generate the others. Many teams store hex for opaque colours because it is compact and universal, and store rgb channels or a perceptual format when they need alpha or computed variations. Whatever you choose, document the rounding rule for any generated value, so two tools do not disagree by one unit in a channel and cause a noisy diff.
Real-world use cases
Matching a brand colour. A designer sends a PDF with CMYK values and a hex for the web. The web needs the hex, and print needs the CMYK. Keep both as official values rather than converting one into the other each time.
Translucent overlays. Modal backdrops, image scrims and hover tints use rgb() with alpha or 8-digit hex. Use a scrim such as rgb(0 0 0 / 0.6) to improve text contrast over a photograph.
Email templates. Many email clients have patchy support for modern CSS. Six-digit hex is the safest choice there, and 8-digit hex or modern syntax may be ignored. Test in the clients your audience uses.
Canvas, SVG and charts. Drawing APIs often take hex strings or rgb() strings. SVG attributes such as fill and stroke accept the same colour syntax. Our SVG formatting and editing guide covers how those attributes sit in a file, and our CSS formatting and architecture guide shows how to organise colour declarations in a stylesheet.
Mobile and native apps. Android and iOS use their own colour representations, and the byte order may differ from CSS, as noted earlier. Check the platform documentation before pasting a web hex.
Print and packaging. Business cards, posters and packaging need CMYK or spot colours specified by the printer. Ask for their colour profile and a proof.
Data visualisation. Charts need palettes that remain distinguishable for colour-blind readers and keep enough contrast against the background. Perceptual colour spaces make it easier to generate evenly spaced scales.
Generated stylesheets and theming engines. When code derives shades, it often converts to HSL or a perceptual space, adjusts lightness, and converts back to hex. Rounding at each step can introduce drift, so keep the high-precision value as the source and round only at output.
Common mistakes
- Forgetting the leading zero. A channel of 7 must be
07. A missing zero produces a five- or seven-digit hex that browsers ignore. - Wrong hex length. Only 3, 4, 6 and 8 digits are valid. The browser silently discards anything else.
- Putting alpha first. CSS uses
#RRGGBBAA. Android-style#AARRGGBBpasted into CSS shifts all the colours. - Mixing syntaxes.
rgb(30, 144 255)is invalid, because commas and spaces cannot be mixed within one function. Also, the slash for alpha belongs only to the space-separated form. - Treating alpha as an integer. In CSS,
rgba(0, 0, 0, 50)is not fifty percent. Alpha above one is clamped to fully opaque. Use 0.5 or 50 percent. - Using
opacitywhen you meant an alpha colour. The property fades the whole element and its children. - Assuming the naive CMYK formula is print-ready. It is a rough approximation. Use colour-managed conversion with the printer's profile.
- Round-tripping and expecting exact values. RGB to CMYK to RGB, or alpha fraction to byte and back, introduces rounding drift.
- Judging contrast by eye. Compute it, especially near the 4.5 and 3 boundaries.
- Ignoring transparency in the contrast check. Compute using the blended colour on the real background.
- Hard-coding hex everywhere. Use tokens so a palette change is one edit.
- Relying on colour alone to convey meaning. Add text or shape.
- Using brand colours outside sRGB without a fallback. Provide an sRGB value that works everywhere.
Step-by-step: convert colours with DevUtilX
When you need a quick answer and do not want to write code, use the DevUtilX colour tools in your browser. They run locally, so the values you enter are not uploaded anywhere.
HEX to RGB
- Open the HEX to RGB converter.
- Paste a hex value such as
#1e90ff. The shorthand form#09fworks too. - Read the red, green and blue values, and copy the
rgb()string. - Compare the result with your hand calculation: 1e is 30, 90 is 144, ff is 255.
RGB to HEX
- Open the RGB to HEX converter.
- Enter the three channel values between 0 and 255.
- Copy the six-digit hex result.
If the output looks wrong, check that no channel is outside 0 to 255 and that you did not enter a percentage in a field that expects a number.
RGBA to HEX (with alpha)
- Open the RGBA to HEX converter.
- Enter red, green, blue and an alpha value between 0 and 1.
- Copy the eight-digit hex. The last two digits are the alpha byte.
- Remember that alpha is rounded to the nearest of 256 steps, so a value of 0.5 becomes
80, which reads back as about 0.502.
HEX to CMYK
- Open the HEX to CMYK converter.
- Paste the hex colour.
- Read the cyan, magenta, yellow and key percentages.
- Treat the result as a quick estimate. For final print production, use colour-managed software with your printer's profile, and ask for a proof.
All of these tools handle the input in your browser, which matters if you are working on an unreleased brand palette. As with any online tool, avoid pasting anything confidential that is not needed for the task.
Code examples in three languages
You have seen JavaScript and Python. For command-line work and quick checks, here is a compact shell approach using printf, which converts a hex pair to decimal and back, and which we ran in a standard shell:
# hex pair to decimal
printf "%d\n" 0x90
# decimal to hex pair, zero padded to two digits
printf "%02x\n" 144
printf "%02x\n" 7
144
90
07
The %02x format is the same zero-padding rule from the by-hand section, and it makes a good one-line sanity check.
For a final cross-check, you can verify the JavaScript and Python functions against each other. Both gave #1e90ff80 for rgba(30, 144, 255, 0.5) and both gave C88 M44 Y0 K0 for #1e90ff with the naive formula, so the implementations agree for these inputs.
Where possible, use a well-tested library for production colour work instead of your own functions, especially for perceptual spaces and gamut mapping, which are much more involved than the simple conversions in this guide.
Performance, security and compatibility considerations
Performance. Colour format has no measurable effect on rendering speed. The browser parses any of them into the same internal representation. Choose for readability and maintainability. Where size counts, hex is the shortest for opaque colours, but the saving is a few bytes per declaration, and compression erases most of it. If you are minifying stylesheets, read our guide to CSS minification and critical CSS. Minifiers often shorten colour values, for example turning #ffffff into #fff, which is safe only for the paired-digit cases described above.
Repaint cost. Animating a translucent colour can trigger repaints, but the choice between rgba() and hex does not change that cost. The effect of an animation depends on which property you animate and how the page is composited.
Security. Colour values are rarely dangerous, but user-supplied colours deserve validation. If your product lets users type a colour that you later place into a stylesheet or an inline style attribute, validate it against a strict pattern, or use a parser, and reject everything else. Injecting an unvalidated string into CSS can let an attacker break out of the declaration and add their own rules. A strict check of the form ^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$ accepts only the valid hex forms.
Compatibility. The space-separated rgb() syntax with a slash for alpha, the 4 and 8-digit hex forms, hwb(), oklch() and color() are all newer than the classic forms. Check browser support for your target audience. Provide a classic fallback declaration before a modern one when the colour matters.
Accessibility. Beyond contrast, respect the user's preferences with prefers-color-scheme, prefers-contrast and forced-colours behaviour where your design allows, and never encode meaning in colour alone.
Colour management in images. Photos often carry an embedded colour profile. A browser converts them for display, while an image without a profile is generally treated as sRGB. If an image colour does not match the CSS colour next to it, a missing or mismatched profile is a common cause.
Troubleshooting guide
Work from the symptom to the cause.
Symptom: the colour is not applied at all
Cause. The hex value has an invalid length, a stray character, or a missing hash sign. Browsers drop invalid declarations without an error.
Fix. Count the digits and keep only 3, 4, 6 or 8. Check for the letter O in place of zero, and paste the value into the HEX to RGB converter, which will reject malformed input.
Symptom: the colour is slightly wrong, with a purple or green cast
Cause. A byte-order mix-up, usually Android-style ARGB pasted into CSS, or BGR order from a graphics API.
Fix. Identify the source's channel order and rearrange the bytes. In CSS the order is red, green, blue, alpha.
Symptom: a translucent colour looks different on different backgrounds
Cause. That is how alpha works, since the result is blended with whatever lies behind it.
Fix. If you need the same appearance everywhere, use an opaque colour. Otherwise, test the blended result on every background, including contrast.
Symptom: text became faint when I added opacity
Cause. You used the opacity property, which fades the whole element and its children, instead of an alpha colour on the background.
Fix. Apply alpha to the specific colour property: background: rgb(0 0 0 / 0.5).
Symptom: the alpha changed slightly after converting to hex and back
Cause. Alpha is stored in 256 steps, so 0.5 becomes 128 out of 255, which reads back as 0.502.
Fix. Keep the fractional alpha as the source of truth, and generate hex only for output. Compare with a small tolerance in tests.
Symptom: the printed colour is duller than the screen
Cause. The screen colour is outside the CMYK gamut, or the conversion did not use the press's profile. A naive formula makes this worse because it knows nothing about the destination.
Fix. Convert with the profile your printer specifies, soft-proof it, and order a physical proof. Accept that some vivid screen colours cannot be printed with standard inks.
Symptom: the same CMYK values look different on two prints
Cause. Different paper, inks, presses and calibration. CMYK numbers are a recipe, not an absolute colour.
Fix. Standardise the printing condition and profile, and compare against a proof.
Symptom: the accessibility checker says the contrast fails but it looks fine
Cause. Your monitor, brightness and eyes are not the benchmark, and the formula measures luminance difference, not appearance on your device.
Fix. Trust the computed ratio near the thresholds. Adjust lightness until it passes, then re-evaluate the design.
Symptom: oklch() or color() does nothing in some browsers
Cause. The browser does not support the format yet, so it discards the declaration.
Fix. Put an sRGB fallback before it and check compatibility data on MDN.
Symptom: my brand colour looks dull in the browser compared with the design file
Cause. The design file may use a wide-gamut space, or an image profile, and CSS hex is sRGB only.
Fix. Use color(display-p3 ...) inside a @media (color-gamut: p3) block with an sRGB fallback, if your brand needs the extra vividness.
Best practices and checklist
- Use six-digit hex for opaque colours in shared code, since everyone recognises it.
- Use the
rgb()function with a slash for alpha, orrgba()if you must support very old browsers, rather than 8-digit hex, when readability matters. Alpha as a fraction is easier to reason about than a hex byte. - Document the alpha byte order if you exchange values with mobile or graphics teams.
- Keep tokens, not literals. Name colours by role, and reference tokens in components.
- Pick one source of truth and generate other formats with a documented rounding rule.
- Test contrast by calculation for every text and background pair, in both themes.
- Never use colour as the only signal.
- Provide fallbacks before modern colour syntax.
- Treat CMYK from simple formulas as an estimate. Use ICC profiles and proofs for production print.
- Validate user-supplied colour strings before they reach CSS.
- Prefer perceptual spaces for generated scales, where support allows.
- Keep the CMYK and hex values for brand colours as separate official values in your brand guide.
A short checklist before you ship a palette:
- Does every text colour pass 4.5:1 on its real background, and large text or UI parts pass 3:1?
- Are translucent colours checked in their blended form?
- Are all hex values 3, 4, 6 or 8 digits?
- Do the tokens have semantic names, with dark mode variants?
- Do modern colour values have fallbacks?
- Is there a documented CMYK or spot-colour value for print, with the profile named?
- Is information conveyed by more than colour?
Comparison: colour formats and when to use them
| Format | Example | Strengths | Weaknesses | Best for |
|---|---|---|---|---|
| HEX (6-digit) | #1e90ff |
Compact, universal, easy to copy | Hard to read, no alpha | Opaque colours, tokens |
| HEX (8-digit) | #1e90ff80 |
Compact with alpha | Alpha byte is unintuitive, order varies by platform | Exchanging values with tools |
rgb() / rgba() |
rgb(30 144 255 / 0.5) |
Readable channels, easy alpha | Longer, sRGB only | Overlays, scrims, computed colours |
hsl() |
hsl(210 100% 56%) |
Intuitive hue and lightness | Uneven perceived lightness | Hand-tuned shades, hover states |
hwb() |
hwb(210 12% 0%) |
Paint-like mixing model | Newer, less common | Simple tinting |
oklch() |
oklch(70% 0.15 250) |
Perceptually even scales | Newer, needs fallback | Generated palettes |
color(display-p3 ...) |
color(display-p3 0 1 0) |
Reaches wider gamut | Needs capable display and fallback | Vivid brand colours |
| CMYK | C88 M44 Y0 K0 | Matches print inks | Depends on profile, not a web format | Print and packaging |
What the numbers hide: a closer look at precision
A few finer points explain why two tools can print different numbers for the same colour.
Percent versus byte. The percentage form rgb(11.8% 56.5% 100%) is rounded in the text. When a browser converts it to a byte, it rounds again. Two rounding steps can land one unit away from the integer you started with. If you want exactness, use integers.
Decimal places. Alpha written as 0.502 and alpha written as 0.5 are not the same, though they look the same on screen. If a tool shows three decimals and another shows two, the mismatch is presentation, not error.
Eight bits is a limit. Smooth gradients across a large area can show visible banding because 256 levels per channel is not many when the change is gradual. Browsers and image formats sometimes add dithering, and higher bit depths exist in photography and video. Hex notation, however, can only express 8 bits per channel.
The hex is not the display. The same triplet sent to different screens produces different physical light. Calibration, brightness settings and colour profiles all affect what you see. The code identifies a position in a colour space, and the display makes its best attempt.
Grey is not always neutral in print. Equal CMY percentages do not give a neutral grey on a press, because the inks are not balanced. Print workflows build neutrals carefully, often with black ink, which is why converting a neutral RGB grey to a pure K value, as the naive formula does, is common practice for text and greys.
FAQ
What is the difference between HEX and RGB?
They describe the same colour in the same space. HEX writes the three channels as hexadecimal pairs, and RGB writes them as decimal numbers. #1e90ff and rgb(30 144 255) are identical.
How do I convert HEX to RGB by hand?
Split the hex into three pairs, then for each pair multiply the first digit by 16 and add the second, using a for 10 through f for 15. For #1e90ff, that gives 30, 144 and 255.
Is #fff the same as #ffffff?
Yes. In three-digit shorthand each digit is doubled, so #fff expands to #ffffff. The same rule gives #f60 as #ff6600.
What does the last pair in an 8-digit hex colour mean?
It is the alpha, or opacity, from 00 (transparent) to ff (opaque). In CSS the order is red, green, blue, alpha, so #1e90ff80 is about 50 percent opaque.
Why does 0.5 alpha become 0.502 after a round trip?
Alpha is stored as an integer from 0 to 255. Half of 255 is 127.5, which rounds to 128, and 128 divided by 255 is about 0.502.
Should I use rgba() or the newer rgb() with a slash?
Both work in current browsers, since rgba() is an alias. Use the modern form in new code if your support targets allow, and the comma form with rgba() if you need to cover very old browsers.
Can I convert RGB to CMYK exactly?
Not in the general case. The simple formula gives an approximation, but real conversion depends on ink, paper, press and the target ICC profile, and some screen colours lie outside the CMYK gamut.
Why do my printed colours look duller than my screen?
Screens emit light and can show more saturated colours than CMYK inks can reproduce. Vivid blues, greens and oranges shift the most. Use a soft proof and a printed proof to see the real result.
What contrast ratio do I need?
WCAG 2 asks for at least 4.5:1 for normal text and 3:1 for large text and interface components at level AA, and 7:1 for normal text at AAA. Check the current guidelines for the precise wording.
Is HSL better than RGB?
It is easier for people to adjust, since lightness and hue are separate controls. It describes the same sRGB colours, though, and its lightness is not perceptually uniform, so for generated palettes a perceptual space such as OKLCH is often better where supported.
What is Display P3 and do I need it?
It is a colour space wider than sRGB that many modern screens can display. You need it only when you want colours more vivid than sRGB allows. Provide an sRGB fallback for other screens.
Does the colour format affect performance?
No measurable difference in practice. Browsers parse them all to the same internal form. Choose for clarity.
Why did my hex colour do nothing?
Most often the length is wrong. Valid forms are 3, 4, 6 and 8 digits, and the browser silently ignores any other length.
Are DevUtilX colour tools safe for private palettes?
They run in your browser and process values locally rather than uploading them. As a habit, avoid pasting anything confidential into any online tool.
Conclusion
HEX, RGB and RGBA are three spellings of one sRGB colour, and converting between them is simple arithmetic once you remember the zero padding, the alpha byte order and the rounding. CMYK is a different kind of thing: a recipe for ink on a particular paper, so a simple formula can estimate it but only a colour-managed workflow can deliver it. Add a computed contrast check and a token-based palette, and your colours will stay consistent, accessible and easy to change.
To convert values right now, try the HEX to RGB tool, then the RGB to HEX, RGBA to HEX and HEX to CMYK converters. To keep going, read our guides on CSS formatting and architecture and SVG formatting and editing.
Further reading
Try the tools
Easily convert HEX color codes to the RGB format for use in web design and digital media. This tool is perfect for developers and designers working with digital colors, ensuring accuracy and consistency.
Quickly convert RGB color codes to the HEX format for use in web design and development. This tool is essential for ensuring color consistency across your digital projects, from websites to applications.
Convert RGBA color codes to the HEX format, which is widely used in web design. This tool is perfect for designers and developers needing to switch between color formats for different applications.
Convert HEX color codes to the CMYK color model, which is essential for print design. This tool ensures your digital colors are accurately represented in printed materials, maintaining brand consistency across media.


