Quick Answer
RGB to HEX conversion turns three decimal channels from 0 to 255 into a six-character hexadecimal code. Convert each channel to a two-digit hex value from 00 to FF and join them behind a hash. So rgb(255, 87, 51) becomes #FF5733. Both notations describe the exact same sRGB color, just written differently.
Converting RGB to HEX means taking a color written as three decimal numbers, like rgb(255, 87, 51), and rewriting it as a six-character hexadecimal code, like #FF5733. You convert each channel, red, green, and blue, from its 0-to-255 decimal value into a two-digit hex value from 00 to FF, then join the three pairs behind a hash. That's the whole thing. Both formats describe the identical color, so nothing is lost in the swap when your channels are whole numbers, which they almost always are. This guide walks through the conversion step by step, explains why the maths lines up so neatly, covers when to use each format, and gives you a reference table. For the reverse direction, see our HEX to RGB complete guide.
What Are RGB and HEX?
RGB stands for red, green, blue. It describes a color by the intensity of each of those three channels, written as a decimal number from 0 to 255, like rgb(255, 87, 51). This maps directly to how a screen works. Every pixel is made of tiny red, green, and blue lights, and mixing them at different brightnesses produces every color you see.
You'll see that written two ways now, and both are correct. The commas are optional. W3C CSS Color Module Level 4 defines the modern form with plain spaces between the channels, so rgb(255 87 51) and rgb(255, 87, 51) mean exactly the same thing. Older tutorials and most design tools still emit the comma version, which is why you'll keep meeting it.
The same change quietly retired rgba(). Instead of a fourth comma-separated value, alpha now goes after a forward slash, so half-opacity is rgb(255 87 51 / 0.5) or rgb(255 87 51 / 50%). The separate rgba() function still works and isn't going away, since browsers keep the legacy grammar for compatibility. But if you're writing new CSS, the slash form is the one the spec leads with.
None of this changes the conversion. Whichever way the commas and slashes fall, the three channel numbers are identical, so every method in this guide applies unchanged. Strip the punctuation, read the three values, convert each one.
A HEX code is the same three channels written in base-16, or hexadecimal, notation. Each channel becomes two characters from 00 (off) to FF (maximum), and the three pairs sit together behind a hash, like #FF5733. Hexadecimal uses the digits 0 to 9 and then the letters A to F to count from ten to fifteen, which lets a single pair of characters cover all 256 values of a channel. HEX is the default output of design tools like Figma and Photoshop, which is why it shows up everywhere on the web. Our HEX color codes explained guide breaks the format down further.
How Do You Convert RGB to HEX?
The conversion is a simple three-step process. Take rgb(255, 87, 51):
- Take each channel on its own. Red is 255, green is 87, blue is 51.
- Convert each to two hex digits. Divide the value by 16 for the first digit and take the remainder for the second, mapping 10 to 15 onto A to F. So 87 divided by 16 is 5 remainder 7, which is 57. And 255 is FF, 51 is 33.
- Join them behind a hash. #FF5733.
One rule to remember: pad any single-digit result with a leading zero so every channel is exactly two characters. A blue value of 5 converts to 5 in hex, but you write it as 05, not 5, or the code breaks. That's the single most common slip when people convert by hand. A free converter pads automatically and removes the risk entirely.
How Do You Convert RGB to HEX in Code?
Two built-in methods do the whole job, and once you've seen them you'll never write the long version again. Here it is in JavaScript:
'#' + [r, g, b]
.map(n => n.toString(16).padStart(2, '0'))
.join('');
toHex(255, 87, 51); // "#ff5733"
Both halves come straight from the language. Mozilla's MDN reference on Number.prototype.toString documents the radix argument as an integer from 2 to 36 setting the base, noting that for bases above 10 "the letters of the alphabet indicate digits greater than 9," so a to f cover ten to fifteen. Pass 16 and you get hexadecimal. And String.prototype.padStart handles the leading-zero rule this guide flagged earlier: it pads a string until it reaches a target length, so "5".padStart(2, "0") gives "05".
That padStart call is the entire reason hand-rolled versions of this function break. Drop it and rgb(5, 10, 20) produces "#51014" instead of "#050a14", which is five characters, silently invalid, and renders as nothing.
The same two ideas in Python, where the format spec does both jobs at once:
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
to_hex(255, 87, 51) # '#ff5733'
Read {:02x} as three instructions: x means hexadecimal, 2 means two characters wide, and the leading 0 means pad with zeros rather than spaces. Swap x for X if you want uppercase.
Three things worth guarding in production code.
- Round before you convert, don't hope.
toString(16)on a non-integer gives you a fractional hex string like "7f.8", not a rounded one. If your values can be fractional, run them through Math.round first. - Clamp to 0 through 255. A value of 300 becomes "12c", three characters, and quietly corrupts the rest of the code. A value below zero produces a minus sign. Clamping is one line and saves a confusing bug.
- Pick a case and stick to it. Both methods output lowercase. Hex codes are case-insensitive, so this is purely about your codebase reading consistently, but mixed case makes string comparison between two identical colors fail.
Put those together and the defensive version is still one line longer than the simple one:
const toHex = (r, g, b) =>
'#' + [r, g, b]
.map(n => clamp(n).toString(16).padStart(2, '0'))
.join('');
One last note if you're pulling colors out of the DOM. getComputedStyle returns colors as an rgb() string rather than hex, so you'll need to parse the numbers out before converting. That's not a browser quirk, it's just the resolved value format, and it's the reason so many projects end up with a function like this one somewhere in their utilities.
Why Does RGB Convert to HEX So Cleanly?
Because both formats are just two ways of writing the same underlying numbers. A color channel is stored in 8 bits, which gives 256 possible levels, from 0 to 255. And 256 is exactly what two hexadecimal digits can hold, from 00 to FF. That neat match is why one RGB channel always maps to exactly two hex characters with no rounding and no loss.
Stack the three channels together and you get 24-bit color: 8 bits each for red, green, and blue, which works out to 256 x 256 x 256, or 16,777,216 possible colors, the "16.7 million" figure quoted for standard displays. Both notations sit inside the same standard color space, sRGB, which was created by HP and Microsoft in 1996 and standardised by the International Electrotechnical Commission as IEC 61966-2-1. The W3C CSS Color Module Level 4 is explicit about it: hex notation "allows an sRGB color to be specified by giving the components as hexadecimal numbers," and the spec spells out the order, with the first pair the red component, the next green, the last blue. That design was deliberate. MIT's CSAIL reference on color spaces describes sRGB as a standard default color space for the internet that was "cleverly designed to employ the 24-bit (256x256x256) color encoding already in widespread use; and the 2.2 gamma intrinsic to CRT monitors." In other words, the numbers lined up with the hardware people already had. Mozilla's MDN reference on CSS color values lists hex and rgb() side by side as two syntaxes for the same thing. So the two are interchangeable by design, not by coincidence.
Does RGB to HEX Ever Lose Anything?
For the colors you'll actually type, no. But the clean mapping above assumes something that modern CSS no longer requires, and it's worth knowing where the edge is.
That assumption is whole numbers. The classic form of rgb() takes three integers from 0 to 255, and those map onto hex perfectly because 256 levels is exactly what two hex digits hold. Every article about this conversion, including the section right above, tells that story. It's true as far as it goes.
What's changed is the grammar. The W3C CSS Color Module Level 4 defines each rgb() channel as accepting a number, a percentage, or the keyword none, and notes that percentage and number components "may be freely mixed." So rgb(255.5 87.2 51) is valid CSS. So is rgb(50% 20% 100%). Neither has an exact hex equivalent, because hex has nowhere to put a fraction of a level. Six characters, eight bits a channel, 256 steps, and that's the lot.
The spec is direct about what happens next: "Implementations should honor the precision of the component as authored or calculated wherever possible. If this is not possible, the component should be rounded towards +∞." Rounding is the mechanism. Take the percentage case: 50 percent of 255 is 127.5, which isn't a level that exists, so something has to give.
Alpha is the other one, and it's the reason this guide says half opacity is "roughly" 80 rather than exactly. Under the spec an alpha value is a number or a percentage clamped to the range 0 to 1, which is continuous. The fourth pair in an 8-digit hex code is eight bits, which is not. So rgba(255, 87, 51, 0.5) becomes #FF573380, and 80 in hex is 128 out of 255, or about 0.502. Close enough that nobody will ever see it, and genuinely not the number you wrote.
Three practical consequences.
- Brand colors and design handoffs are unaffected. Figma, Photoshop, and every style guide hand you integers. Integers round-trip perfectly, forever. Don't let any of this make you nervous about pasting a hex code.
- Generated colors are where drift lives. If you compute a color in code, convert it to hex, read it back, adjust it, and convert again, small errors accumulate. Do the maths in one format and convert once at the end rather than round-tripping through hex at every step.
- Don't diff hex to check whether two computed colors match. Two colors that differ by a fraction of a level land on the same hex code, and two that are visually identical can land on different ones. Compare the source values instead.
None of that changes the answer to the question this guide is about. Convert your integers and the hex code is exact. Just know the precision ceiling is in the format, not in your tool, so a converter that gives you a slightly different last digit than you expected is probably rounding correctly rather than getting it wrong.
What If Your RGB Values Aren't 8-Bit sRGB?
Everything up to here assumes your input is three integers from 0 to 255 in sRGB. That covers most cases. But you'll hit values that don't fit that shape, and the trap is that the arithmetic still works, so you get a hex code back that looks fine and is wrong.
Three situations to watch for.
Floats from 0 to 1. Shaders, game engines, Swift, and plenty of design tooling hand you colors as decimals like 0.2, 0.6, 0.86 rather than 51, 153, 219. Multiply each by 255 and round to the nearest whole number, then convert as normal. Round rather than truncate, because chopping the decimal biases every channel downward and the whole palette drifts slightly dark. Modern CSS is relaxed about input formats here, and the MDN reference on the rgb() function is the place to check what notation is accepted where.
16-bit channels. Open a file in a 16-bit editing mode and your channels run 0 to 65535 instead of 0 to 255. The instinct is to divide by 256. Divide by 257. The reason is small but exact: 65535 divided by 255 is 257 precisely, so dividing by 257 maps the top value to 255 and the bottom to 0 with nothing left over. Divide by 256 and your whitest white comes out at 254, which is the kind of error nobody notices until a background doesn't match.
RGB from a different color space. This is the one that actually bites, and it connects to the sRGB ceiling described further down. If your numbers came from a Display P3 or Adobe RGB source, running them through the hex conversion doesn't convert anything. It relabels P3 numbers as sRGB numbers, and since hex means sRGB by definition, the result renders as a genuinely different color rather than an approximation of the original. The W3C's CSS Color Level 4 list of predefined color spaces shows how many distinct spaces are now addressable, and moving between them takes a real color-space conversion rather than the six-character rewrite this guide describes.
So one habit covers all three. Before converting, ask what your numbers actually are: what range, and what space. If they're 0 to 255 and sRGB, everything above applies unchanged. If they're not, fix the range first and the space second, and only then reach for the hex.
Where Did Your RGB Numbers Actually Come From?
The section above ends by telling you to ask what your numbers are, what range and what space. Fair advice, and useless until you can answer it. So here's the answer for the way most people get an RGB value in the first place, which is by pointing something at a screen and picking a pixel.
Screen sampling is where two people looking at the same color end up typing different numbers. And neither of them is doing anything wrong.
Start with the one case that's unambiguous, because it sets the standard the others fall short of.
Browsers have their own picker. The EyeDropper API lets a page open an eyedropper and sample a color from anywhere on the user's screen, including outside the browser window. Look at what the promise resolves to, though, because the property name is the interesting bit. It's called sRGBHex, documented as "a string representing the selected color, in hexadecimal sRGB format". Not hex. Not color. The API puts the color space in the name of the field, because a hex string without one is an assertion rather than a measurement.
That's the behaviour you want from any picker. Most of them don't offer it.
You also can't count on the API itself. The web-features explorer lists Eyedropper as Limited availability, and the support table is bleak: Chrome 96, released 15 November 2021, and Edge 96 four days later. Firefox, not supported, with a stated position raising concerns about integration and API design. Safari, not supported, with concerns about API design and maintenance. Not on Chrome for Android, not on Safari on iOS. Chrome Platform Status puts usage at roughly 0.003 percent of page loads. MDN is blunt about the status too, marking it experimental and noting it "is not Baseline because it does not work in some of the most widely-used browsers".
Five years is long enough that this isn't a feature you're waiting on. It's one you design around.
So people use the operating system's picker, or take a screenshot and sample that. Which is where the numbers start moving.
The reason is the thing the International Color Consortium exists to manage: a set of RGB numbers doesn't describe a color until you know which space they're in. A profile is what supplies that missing half. On a wide-gamut display, what's on the glass has already been transformed out of sRGB into the display's own space. An OS picker reading the panel, or a screenshot capturing it, can hand you numbers describing that transformed version. They're accurate numbers. They're just answering a different question than the one you asked, which was "what did the designer type".
Then you paste them into a converter, get a hex code, and ship a color that is not the color. The arithmetic in this guide worked perfectly. The input was wrong before it arrived.
There's a ten-second test that tells you which situation you're in, and it's worth doing once on every machine you work on.
Open a browser tab with a pure red background, the plain #FF0000. Sample it with whatever picker you normally use. If you get 255, 0, 0, your pipeline is clean and you can trust it. If you get anything else, 234-something or 255 with a stray green value, your picker is reporting in your display's space rather than sRGB. Nothing is broken. But every reading you take with that tool is shifted by the same transform, and now you know.
Practical rules that follow from all of this.
- Ask for the value, don't sample it. The designer has the number. The CSS has the number. A design file has the number. Every one of those beats reading pixels, and it takes less time than the eyedropper does.
- Sample inside the browser if you can. DevTools pickers and the EyeDropper API both work in the page's color space. OS-level tools work in the display's.
- Trust a picker that names its space, and be careful with one that doesn't. A tool that says "sRGB" beside the number has told you what it means. A bare hex code has not.
- Treat a screenshot as a photo of a color, not the color. It carries whatever profile the capture applied, and sampling it re-reads that, not the original.
- Run the red test once per machine. Ten seconds, and it settles the question permanently for that setup.
None of this changes the conversion. Give this guide three sRGB integers and the hex code it produces is exact, every time. The point is narrower and easier to act on: the conversion is the reliable part of your workflow, and the pixel you sampled is not.
What Happens When You Need That Color in Print?
It stops being a conversion and becomes a translation with judgement calls in it. Everything above this point has been arithmetic you could do on paper. Print isn't that, and the difference catches people out at exactly the wrong moment, usually the week the brochures are due.
Here's the root of it. HEX isn't a different color model, it's the same three numbers written in base 16, which is why the round trip is exact. CMYK is a genuinely different model. Screens add light, ink subtracts it, and what a given press can actually produce depends on the machine, the ink and the paper it's sitting on. So there's no formula waiting to be looked up. There's a profile.
That's what the International Color Consortium exists to standardise. Device profiles, in their words, "provide color management systems with the information necessary to convert color data between native device color spaces and device independent color spaces". Everything routes through what they call the Profile Connection Space, described as the interface providing an unambiguous connection between the input and output profiles, built on CIE 1931 colorimetry at the D50 white point.
Then comes the part that surprises developers. The ICC defines four rendering intents, which are four different answers to the question of what to do with a color the printer can't reach:
- Media-relative colorimetric. Rescales white points between the actual and reference media.
- ICC-absolute colorimetric. Keeps chromatically adapted values unchanged, which suits spot colors and proofing.
- Perceptual. Prioritises preserving detail across the tonal range for general image reproduction.
- Saturation. Trades hue accuracy for vividness.
The ICC labels the last two vendor specific. Read that carefully, because it means two tools can convert the same file and produce two different results, and neither one is broken. There is no single correct CMYK value for your hex code. That's not a gap in the tooling, it's the nature of the problem.
What this looks like in practice is a color coming back duller than the screen promised. The brightest saturated things you can display, vivid greens especially, then oranges and electric blues, simply sit outside what four inks can mix. A pure #00FF00 doesn't exist in CMYK. It gets mapped to the closest thing available, and the closest thing available is noticeably less green.
So if a color matters enough to be a brand color:
- Specify print separately. Your brand guide should carry a print spec alongside the hex, not leave the printer to guess from a screen value.
- Ask which profile they use, then let them convert. They know their press and paper. A CMYK build you generated in a design tool against a generic profile is a worse starting point than their own.
- Consider a spot color if the color is the brand. Mixed as its own ink rather than built from four, which is why so many recognisable brand colors are specified that way.
- Proof on the actual stock. The same file on coated and uncoated paper gives you two different colors. Uncoated soaks up ink and reads flatter.
None of this touches the conversion this guide is about. Your integers still map to hex exactly, and for anything living on a screen that's the whole story. It only matters the moment the color has to leave the screen. Our guide on choosing brand colors covers specifying a palette that survives both, and hex codes explained goes deeper on why the notation works the way it does.
Which HEX Formats Are Actually Valid?
Six digits is what you'll write most days, but the spec allows four different lengths, and knowing them saves confusion when you meet a code that looks too short.
- Six digits, #RRGGBB. The standard form. Two digits per channel, fully opaque. #FF5733.
- Three digits, #RGB. Shorthand where each digit is doubled, so #123 means exactly #112233. It only works when both digits in every pair happen to match, which is why #FF5733 has no shorthand but #FFCC00 collapses to #FC0.
- Eight digits, #RRGGBBAA. Six digits plus a fourth pair for alpha, where 00 is fully transparent and FF is fully opaque. This is how you write an rgba() color as hex.
- Four digits, #RGBA. The doubled shorthand for the eight-digit form, same rule as the three-digit one.
One thing worth knowing about the shorthand: it's not a rounding or an approximation. #FC0 and #FFCC00 are the identical color, byte for byte. And case never matters. The spec treats #00ff00 and #00FF00 as the same value, so pick a convention for your codebase and stick to it for readability rather than correctness.
Do CSS Named Colors Have Exact HEX Values?
Every single one, with no rounding involved. A named color like tomato isn't an approximation the browser interprets. According to MDN, all named colors specify a color in the sRGB color space, which means each one is a fixed RGB triple underneath. tomato is exactly rgb(255, 99, 71), which converts to exactly #FF6347 using the same method as the rest of this guide.
The list grew in an odd way, and that explains most of its weirdness. CSS Level 1 defined 16 basic colors. orange arrived in CSS Level 2. Everything after that came from X11 color names that browser vendors bolted on because designers found 16 far too few. MDN notes the catch: because manufacturers sometimes tailored X11 colors to their own hardware, a CSS keyword's RGB value may not match the same-named color on an X11 system.
Three things about the list are worth knowing before you rely on it:
- darkgray is lighter than gray. Not a typo.
grayis #808080 anddarkgrayis #A9A9A9. Higher channel values, lighter color. It's baked into the spec and nobody can fix it now. If you pick greys by name rather than by value, you will eventually ship the wrong one and spend a while wondering why. - Both spellings are valid. gray and grey are aliases, and so are darkgray, dimgray, lightgray, slategray, darkslategray and lightslategray with their grey variants. Pick one spelling and hold the line, because a codebase carrying both is genuinely irritating to search.
- Names are case-insensitive. A named color is a CSS identifier, so TOMATO and Tomato and tomato all resolve to the same thing. Hex is case-insensitive too, so this is one of the few places where the two formats behave identically.
And one of them has a story behind it. rebeccapurple, #663399, exists because of a Call for Consensus that CSS Working Group co-chair Daniel Glazman posted to the W3C www-style list on 19 June 2014, proposing the name as a tribute to Eric Meyer's daughter Rebecca. Meyer had already given his blessing, and Mozilla, Apple, Google and Microsoft had all committed to shipping it. It's sitting in your browser right now, and it converts to hex exactly like every other color here.
So when should you actually type a name instead of a value? Prototyping and debugging, mostly. border: 1px solid red while you hunt down a layout problem beats looking anything up. But convert to hex or a design token before it reaches production, because a name carries no information about the value to whoever reads the code next, and the grey situation above means they can't even reason about it from the name.
Does an 8-Digit HEX Code Mean the Same Thing Everywhere?
No, and this is the one that silently breaks things. The four lengths in the section above are the CSS rules. Android has four formats too, they look identical, and one of them means something completely different.
Start with the web side. Mozilla's MDN reference on the CSS hex-color type lists #RGB, #RGBA, #RRGGBB and #RRGGBBAA, and describes the alpha component as the one "indicating its transparency," sitting after the blue channel in both the four and eight-digit forms. Alpha last.
Now Android. Its documentation on color resources says a value "always begins with a pound (#) character, which is followed by the Alpha-Red-Green-Blue information," and lists #RGB, #ARGB, #RRGGBB and #AARRGGBB. Alpha first.
Same four shapes. Opposite end for the alpha. And nothing in the string tells you which convention it was written under.
Android's own documentation gives an example that shows how bad this gets. It defines #80ff0000 as translucent red, which reads as alpha 80, then ff0000. Paste that same string into CSS and a browser reads it as RR=80, GG=ff, BB=00, AA=00. That's a bright chartreuse at zero opacity. Not a slightly wrong red. A different color entirely, rendered fully transparent, so what you actually see is nothing at all.
Here's why it catches good developers. Six-digit codes travel between the two platforms perfectly, so most of your palette moves without a hitch and you never learn there's a rule. Only the transparent ones break. And they break by vanishing rather than by looking wrong, which is the single hardest failure to catch in a review, because an element that renders invisibly looks a lot like an element you forgot to style.
Four habits keep you out of it:
- Keep alpha out of the hex string. Store the six-digit color and the opacity as separate values in your design tokens. Then there's no ordering question to get wrong.
- Convert at the boundary, deliberately. Moving a color between web and mobile is a conversion step, not a copy-paste. Treat it like one.
- In CSS, prefer the slash form.
rgb(255 0 0 / 0.5)puts the alpha somewhere unambiguous. An eight-digit hex is compact, but compact is what caused this. - If you must move an eight-digit code, move the pair. Rotating AA from the front to the back, or the reverse, is the whole conversion. Just do it consciously rather than assuming it copied cleanly.
The wider lesson is worth carrying beyond Android. Six-digit hex is genuinely universal, which is a large part of why it won. Eight-digit hex is not, so treat any eight-digit code as platform-specific until you know where it came from.
When Should You Use RGB vs HEX?
Both are fully supported in CSS and describe the same colors, so the choice is about what's convenient for the job in front of you.
- HEX is best for pasting exact colors from a design tool or a brand style guide, and for keeping stylesheets compact. It's the format most designers hand off.
- RGB and RGBA shine when you need transparency or when you're generating colors in code. The decimal numbers are easy to read, and rgba(255, 87, 51, 0.5) sets 50 percent opacity cleanly.
- HSL is worth knowing too. It uses hue, saturation, and lightness, which makes it the easiest format for building darker hover states or muted variants. Many design systems store palettes in HSL for exactly that reason.
A common workflow is to store brand colors as HEX, convert to RGBA when you need a see-through overlay, and reach for HSL when building tints and shades. Our CSS color formats explained guide goes deeper on all of them.
Advertisement
Can You Tell How Light a Color Is From Its HEX Code?
No, and this is the thing the conversion quietly hides. The maths above is exact, but exact isn't the same as meaningful. Six characters tell you the channel values. They don't tell you how bright the color looks.
Take #00FF00 and #0000FF. Identical structure, one channel at full strength in each. Green is dramatically brighter to your eye than blue. Nothing in the digits says so.
Two reasons for the gap, and both matter if you ever check contrast.
The channels aren't equal. The W3C's WCAG working group defines relative luminance as L = 0.2126 R + 0.7152 G + 0.0722 B. Green carries roughly ten times the weight of blue. So a change to the middle pair of your hex code moves perceived brightness far more than the same change to the last pair, even though they look equally significant written down.
The values aren't linear either. Before those coefficients apply, each channel gets divided by 255 and then linearised: if the result is at or below 0.03928 you divide by 12.92, otherwise you compute ((value + 0.055) / 1.055) raised to 2.4. Which is why doubling a hex channel doesn't double anything you can see. MDN's accessibility guide on colors and luminance describes relative luminance as "the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white," and puts it plainly: "Calculations for relative luminance are not casual ones."
Contrast then compares two of those numbers. WCAG defines the ratio as (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter color. Note that neither hex code appears anywhere in that calculation. You have to convert, normalise, linearise, weight, and only then compare.
What that means for you day to day is short. Don't eyeball accessibility from hex codes, and be especially careful with pairs that look far apart on paper but sit close in luminance. Run the actual check instead, with our contrast checker, and if you want the thresholds and what they apply to, our contrast accessibility guide and WCAG color guide cover them.
This is also the cleanest argument for the formats in the next section. Hex and rgb() store what the screen should emit. Something like oklch() stores how light the color should look, which is why stepping its lightness value by a fixed amount produces steps your eye reads as even, and stepping a hex channel by a fixed amount doesn't.
Is HEX Still Enough in 2026?
For most work, yes. But there's a real ceiling now, and it's worth knowing where it sits.
Everything above this section is about notation. HEX and rgb() write the same numbers differently, and converting between them loses nothing. What they share is a limit: both are locked to sRGB, and that goes right back to the beginning. The W3C's CSS FAQ records that sRGB "was developed by the ICC (International Color Consortium) and adopted by W3C for use in CSS", and that in CSS1 the sRGB model is what defines the exact, device-independent color of expressions like #A7C7FF. Mozilla's MDN reference on the CSS color() function is blunt about it, noting that color() lets you specify a color in a named color space "rather than the implicit sRGB color space that most other color functions operate in." Hex and rgb() are those other functions. There is no hex code for a color outside sRGB, because there's nowhere in six characters to say which space you meant.
That mattered less when every screen was roughly an sRGB screen. It matters more now that most recent phones, laptops, and monitors cover Display P3, a noticeably wider gamut. The most saturated reds, greens, and cyans those panels can produce simply have no hex representation. Write #FF0000 and you get the reddest red sRGB has, not the reddest red the display can do.
CSS Color Level 4 fixes this by letting you name the space. The color() function takes a space and its coordinates, so color(display-p3 1 0 0) asks for full-strength red in P3 rather than in sRGB. MDN lists the predefined spaces it accepts, including srgb, display-p3, a98-rgb, prophoto-rgb, rec2020, and the CIE XYZ spaces. There's also oklch(), which describes a color as lightness, chroma, and hue in a perceptually uniform way, so equal numeric steps look like equal visual steps. That's why it's become the format of choice for generating tints and shades, and our monochromatic scheme guide uses it for exactly that.
So what should you actually do? Nothing dramatic.
- Keep using HEX for brand colors and handoffs. It's still what design tools output and what everyone reads fluently. Nothing here makes it wrong.
- Declare an sRGB fallback first. Put the hex value on one line and the wide-gamut value after it. A browser that doesn't understand the second line ignores it and keeps the first, which is the whole mechanism CSS gives you for this.
- Reach for the wider gamut when saturation is the point. A vivid accent, a gradient, a product shot. For body text and UI chrome, sRGB is fine and always will be.
- Use oklch() when you're generating colors, not picking them. Building a ramp of nine shades from one base is where perceptual uniformity earns its keep.
One trap comes with that last point, and it's the one that catches people generating ramps. Because oklch() isn't tied to sRGB, it will happily accept a lightness and chroma combination that no sRGB screen can show. Build nine shades by stepping chroma evenly and a few of the most saturated steps can quietly land outside the space your hex fallback lives in. The spec has a name for this. W3C CSS Color Module Level 4 defines an out-of-gamut color as one that "may be a valid color but still be outside the range of colors that can be produced by an output device (a screen, projector, or printer)," and devotes a section to gamut mapping, describing several approaches to it including binary search, edge-seeking, and ray tracing methods. That there are several is the tell: this is an area still being worked out rather than one fixed behaviour you can rely on looking identical everywhere.
Practically, that means preview a generated ramp rather than trusting the numbers, and keep chroma conservative on any step that needs a matching hex fallback. If two adjacent shades look the same on your monitor but different on someone else's, out-of-gamut steps are the first thing to check.
The short version: HEX isn't going anywhere, and converting RGB to HEX is still the right answer to the question this guide is about. Just know that hex describes one color space, and in 2026 it's no longer the only one your users' screens can show.
Can CSS Do the Conversion For You?
Increasingly, yes. Which is a strange thing to say in a conversion guide, so let's be precise about what it does and doesn't replace.
CSS now has relative color syntax. You hand a color function an existing color with the from keyword, and it pulls that color apart into named channels you can use. Per MDN's guide to relative colors, the origin color gets converted internally into a form the output function understands, then destructured into channel values. The shape is color-function(from origin-color channel1 channel2 channel3 / alpha).
The origin can be a hex code. That's the part that matters here:
/* take a hex, get its RGB channels back */
rgb(from #663399 r g b)
/* same hex at 40 percent opacity, no manual maths */
rgb(from #663399 r g b / 40%)
/* swap one channel, keep the rest */
rgb(from #663399 255 g b)
Read the second one again. That's the rgba conversion this guide explains by hand, done by the browser, with the hex left readable in the source. You don't compute anything and you don't leave a magic number in your stylesheet for the next person to reverse-engineer.
It also crosses color spaces, which is what closes the loop with the previous section. MDN is explicit that the origin's own space doesn't restrict the output function, so lab(from #ff0000 l a b) is valid. Meaning your brand hex can feed an oklch ramp directly:
/* one hex in, a lighter and a darker step out */
--brand: #663399;
--brand-light: oklch(from var(--brand) calc(l + 0.12) c h);
--brand-dark: oklch(from var(--brand) calc(l - 0.12) c h);
MDN's channel table is worth knowing before you write this: rgb() gives you r g b alpha, hsl() gives h s l alpha, and oklch() and lch() give l c h alpha. Use the wrong letters for the function you're in and it won't parse.
One syntax trap catches nearly everyone the first time. Channel values resolve to plain numbers, never percentages, so calc(l + 20) is fine and calc(l + 20%) is not. If a relative color silently fails to apply, that's the first thing to check.
And the honest limits, because this doesn't retire the converter:
- Browser support isn't universal yet. Declare a plain fallback on the line above, same pattern as the wide-gamut advice further up. A browser that can't parse the relative color keeps the previous declaration.
- It only works inside CSS. The moment you need a hex for a design tool, an email template, a brand document, an app manifest, or anything outside a stylesheet, you're converting the old way.
- It computes, it doesn't tell you. The browser resolves the value at render time. If you want to know what
rgb(63, 81, 181)actually is in hex so you can write it in a spec, you still need the conversion.
And before you reach for relative color, check whether you actually need it. Most of the time the job is "make this a bit lighter" or "make this a bit darker," and there's a simpler function for that with better support behind it.
MDN documents color-mix() as taking two or more colors and returning the result of mixing them in a given color space by a given amount. Mixing with white lightens, mixing with black darkens, and your hex goes straight in:
--brand: #663399;
--brand-light: color-mix(in oklab, var(--brand) 80%, white);
--brand-dark: color-mix(in oklab, var(--brand) 80%, black);
Two details worth knowing. If you leave the color space out, MDN says it defaults to oklab, which is the one you want for even-looking steps anyway. And percentages get normalised for you if they don't add up to 100, so you can write one and let the other fill in.
The support gap is the real argument, though. Per the WebDX explorer, color-mix() has been Widely available since 9 November 2025, the same milestone oklch() reached. Relative color syntax is only Newly available and isn't projected to reach Widely available until 16 March 2027. So for the common lighten and darken case, color-mix() ships today with less to think about.
Keep relative color for what it's genuinely better at: reaching into individual channels, swapping one and holding the rest, or converting a hex into another space's channel values. Mixing can't do any of that.
So the rule of thumb: convert once to get your canonical hex, then let CSS derive everything downstream from it, with color-mix() for simple lighter and darker steps and relative color syntax when you need channel-level control. That way one value is authoritative and the variants can't drift out of sync with it.
Can You Actually Ship These in Production Yet?
Mostly yes, and the two features above are at different stages. That distinction is the difference between writing a fallback and not bothering.
The useful measure here is Baseline, the browser support system run by the W3C WebDX Community Group. It tracks a core browser set of Chrome on desktop and Android, Edge, Firefox on desktop and Android, and Safari on macOS and iOS. A feature is Newly available once all of those support it, which makes it interoperable. It becomes Widely available when, in web.dev's words, "30 months have passed since the newly interoperable date. The feature can be used by most sites without worrying about support."
Here's where the modern color features sit, according to the WebDX web features explorer:
- oklch() and oklab(): Widely available since 9 November 2025. Safari got there first in 15.4 back in March 2022, followed by Chrome and Edge 111 in March 2023 and Firefox 113 in May 2023. Use them without a second thought.
- Relative color syntax: Newly available since 16 September 2024, per the same source's relative color entry. Chrome and Edge 125 shipped it in May 2024, Firefox 128 in July 2024, and Safari 18 completed the set in September 2024. It's projected to reach Widely available on 16 March 2027.
Read those dates rather than the labels. Newly available means every current browser handles it, not that every browser your visitors are running handles it. Someone on an iPhone that stopped at iOS 17, or a work laptop frozen on an old Chrome build, will hit relative color syntax and get nothing. That's a much smaller group than it was, and it isn't zero.
The fix costs two lines. Declare the plain value first, then upgrade it:
:root {
--brand: #663399;
--brand-light: #7d47b5; /* converted once, by hand */
}
@supports (color: rgb(from white r g b)) {
:root {
--brand-light: oklch(from var(--brand) calc(l + 0.12) c h);
}
}
Old browsers read the first block and stop. New ones apply the second and get the derived version. And notice what the fallback line is: a hex code you produced with the conversion this guide explains. That's the practical answer to why any of this still matters in 2026. The modern functions are the ceiling, but hex is the floor everything lands on when the ceiling isn't there.
One more reason hex keeps its place. Baseline only describes browsers. Plenty of things that need a color from you aren't browsers at all, including email templates, printed spec sheets, native mobile config files, and the brand guidelines PDF somebody will open in four years. Hex works in all of them.
How Do You Handle the Same Color in Dark Mode?
Not by inverting the hex. That's the instinct, and it produces the muddy, glaring results you've seen on sites that clearly bolted dark mode on at the end.
The reason sits in the luminance section above. A hex code is a coordinate in a color space, not a brightness dial. Flipping #FFFFFF to #000000 works because those two happen to be endpoints. Doing the same arithmetic to #2E7D32 gets you a color with no relationship to the one you started with.
So you need two values per color. The good news is that the plumbing for that got much simpler recently.
CSS now holds both values in one place
MDN describes the light-dark() function as one that "accepts two colors or two images and returns a color or an image based on the active color scheme, without needing a prefers-color-scheme media feature."
It returns the first value when the used color scheme is light or when no preference is set, and the second when it's dark. One requirement catches people out: the color-scheme property has to be set to light dark, normally on :root, or the function won't do anything.
On availability, MDN puts it at Baseline 2024, newly available, working "since May 2024" across the latest devices and browser versions. Which places it in exactly the category the production section above describes: broadly safe now, still worth a fallback if your traffic includes older browsers.
The practical gain is that your two hex values live in one declaration instead of in a stylesheet and a media query that drift apart over time.
Choosing the two values is the part nobody has solved
Here the evidence is far less settled than the confident advice online suggests.
Zack While and Ali Sarvghad ran a study presented at IEEE VIS in 2024, testing 134 participants, 69 under the age of 60 and 66 aged 60 and over, on analysis tasks across bar, line and scatter charts in both polarities, measuring accuracy and completion time.
Three findings are worth carrying.
First, there was no winner. They report "each polarity benefiting comparable proportions of participants." Roughly as many people did better in dark mode as in light.
Second, and this is the uncomfortable one, "the contrast polarity that led to better performance did not always match their preferred polarity." What people liked was not a reliable guide to how they actually performed, which includes you and your designer.
Third, the stakes aren't trivial. They found the choice "can have an impact on time similar to that of the choice of visualization type, resulting in an average percent difference of around 36%." Picking the wrong polarity cost about as much as picking the wrong chart.
They also found that "the effects of contrast polarity on visual analysis performance do not noticeably change with age", which cuts against the common assumption that older users all want light mode.
Fair caveats: this was a short paper measuring chart-reading tasks, not long-form text, so read it as being about dashboards and data-dense screens rather than about novels.
What to actually do
- Ship both and let people choose. If the winner varies person by person, hard-coding one polarity is a decision you don't have the evidence to make. Respect
prefers-color-scheme, and offer an explicit toggle on top of it. - Don't use preference as a proxy for quality. Including your own. Liking a theme and reading it well are separate things, and the study found they come apart.
- Pick the dark value, don't calculate it. Pull saturation down rather than up, avoid pure black backgrounds under pure white text, and check the result rather than trusting a formula.
- Test anything data-dense in both. A 36 percent swing in task time is not a rounding error, and it's invisible if you only ever look at your own preferred mode.
For the contrast-ratio side of this, which is its own discipline, our color contrast accessibility guide works through the thresholds properly. This section is about getting two correct hex values into one stylesheet. That guide is about whether either of them is readable.
What Are Common RGB to HEX Values?
A handful of colors come up again and again. Here's a quick reference with both notations side by side.
| Color | RGB | HEX |
|---|---|---|
| Black | rgb(0, 0, 0) | #000000 |
| White | rgb(255, 255, 255) | #FFFFFF |
| Red | rgb(255, 0, 0) | #FF0000 |
| Green (lime) | rgb(0, 255, 0) | #00FF00 |
| Blue | rgb(0, 0, 255) | #0000FF |
| Yellow | rgb(255, 255, 0) | #FFFF00 |
| Orange | rgb(255, 165, 0) | #FFA500 |
| Grey | rgb(128, 128, 128) | #808080 |
| Off-white | rgb(245, 245, 245) | #F5F5F5 |
| Navy | rgb(30, 58, 95) | #1E3A5F |
Look at the pure colors and the pattern jumps out. A channel that's fully on becomes FF, and one that's off becomes 00. Red is 255 red and nothing else, so #FF0000. White is all three channels maxed, so #FFFFFF. Grey at 128 lands on 80 in hex, which is why balanced greys read as #808080. Once you can see that, you can estimate many HEX codes from their RGB values in your head.
How Do You Use a Free RGB to HEX Converter?
The fastest way to convert is to let a tool do the maths, especially since it pads zeros and handles the letters for you. Our converter runs entirely in your browser, so nothing is uploaded and there's no signup:
- Open the tool. Go to the free color converter, which works in both directions.
- Enter your RGB values. Type the red, green, and blue numbers, each from 0 to 255.
- Read the HEX result. The tool shows the matching HEX code, usually with a live preview swatch so you can confirm the color.
- Copy and paste. Drop the HEX code straight into your CSS or design file.
If you'd rather grab a color off the screen first, the color picker gives you both the RGB and HEX values at once. Between the two, you'll rarely need to do the conversion by hand again.
What Else Do People Ask?
How do you convert RGB to HEX?
Take each of the three RGB channels, a decimal number from 0 to 255, and convert it to a two-digit hexadecimal value from 00 to FF, then join them behind a hash. For example, rgb(255, 87, 51) becomes 255 which is FF, 87 which is 57, and 51 which is 33, giving #FF5733. Pad any single-digit result with a leading zero so each channel is two characters.
What is the difference between RGB and HEX?
They describe the same colors in different notations. RGB writes each of the red, green, and blue channels as a decimal number from 0 to 255. HEX writes each channel as a two-digit hexadecimal value from 00 to FF. RGB is easier to read and supports an alpha channel for transparency, while HEX is compact and standard in design tools.
Is RGB or HEX better for CSS?
Neither is better, since both are fully supported in CSS and describe the same sRGB colors. HEX is convenient for pasting exact brand colors from design tools and keeps stylesheets tidy. RGB is handy when you need transparency through rgba or when generating colors in code. Many developers use HSL for adjusting a color's lightness and saturation.
Can every RGB color be written as HEX?
Yes, every standard RGB color converts to a HEX code with no loss, because both use the same 8-bits-per-channel sRGB values. The only exception is the alpha channel: an rgba color with partial transparency needs an 8-digit HEX code to carry the opacity, which some older tools support less reliably than rgba.
What is rgba and how does it become HEX?
rgba is RGB plus an alpha value between 0 and 1 that sets opacity, like rgba(255, 87, 51, 0.5) for 50 percent. To write it as HEX you convert the three color channels normally, then add a fourth two-digit pair for the alpha, making an 8-digit code. Half opacity is roughly 80 in hex, so the example becomes #FF573380.
Sources: W3C CSS Color Module Level 4 (w3.org) for hex and rgb() notation in the sRGB space, for the definition of an out-of-gamut color, for the gamut mapping section and the several approaches it describes, for the rgb() grammar accepting numbers and percentages, and for the rounding and clamping rules on components and alpha; MIT CSAIL's color spaces reference (people.csail.mit.edu) on the 24-bit 256x256x256 encoding and the CRT gamma it was built around; MDN Web Docs on CSS color values, on the color() function and its predefined color spaces, on Number.prototype.toString and its radix argument accepting an integer from 2 to 36, on String.prototype.padStart for the leading-zero rule, and on the CSS hex-color type listing #RGB, #RGBA, #RRGGBB and #RRGGBBAA with alpha as the final component (developer.mozilla.org); Android developer documentation on color resources, which states values are followed by "Alpha-Red-Green-Blue" information in the formats #RGB, #ARGB, #RRGGBB and #AARRGGBB, placing alpha first, and gives #80ff0000 as translucent red (developer.android.com). All linked above. The sRGB color space itself is standardised as IEC 61966-2-1, which sits behind a paywall at iec.ch, so we describe it rather than link it.