Skip to main content
Photography of Alvaro Montoro being a doofus
Alvaro Montoro

Spanish Longhorn

paintbrushes

The Ultimate Guide to CSS Colors (2020 Edition)

css html

I know this article is really long, so I split it in a series of smaller articles. If you want to keep reading this one, you can use this table of contents to jump to the different sections:

Introduction

There are plenty of great articles online that explain colors in CSS. They focus on RGB, HEX, HSL, and named colors and give detailed descriptions of each of them. So, if those articles are great... why should you read this one? How is it different?

One of the beauties of HTML, CSS, and Javascript is that they are in a state of continuous change and improvement: things that were broken are fixed, new standards are added, old ones are removed... And the colors in CSS are no exception.

CSS colors have gone through many changes over time and, although the core remains, new changes were introduced over the past few months that may shape CSS differently in the future and we should know.

For example:

  • You have used the comma-separated syntax rgb(255, 255, 255)... but did you know that it is considered legacy, and the notation moving forward should be space-separated: rgb(255 255 255)?

  • You know the functions rgb() and rgba()... but did you know that those functions are synonyms? So you can do rgb(255, 0, 0, 1) or rgba(255, 0, 0) and get the same result.

  • You know about RGB, HEX, and HSL colors... but have you heard about HWB, LAB, LCH, or CMYK colors in CSS?

The idea of this article is not only to review the basics for colors in CSS –there are already great articles like these from CSS-Tricks or DEV–, but also to explore new additions, format changes, or peculiarities that happened over the past few months and became available in the latest CSS Color Module Level 4 Editor's Draft.

For this article, I ran color tests on the following browsers:

  • Chrome and Chromium-based browsers (Brave, Edge) on Mac and Windows
  • Firefox on Mac
  • Safari on Mac
  • Edge (not Chromium) on Windows
  • Chrome on Android
  • Safari on iOS

Let's start with the classics:

RGB / RGBA

RGB stands for Red-Green-Blue. It is a format in which the developer provides values for red, green, and blue separately, and that can have an optional alpha argument to indicate alpha/opacity (thus RGBA).

Traditionally, the rgb and rgba color functions had all the arguments separated by commas (e.g. rgb(255, 0, 0)); but that notation is now considered legacy, and moving forwards all the color functions will have arguments separated by spaces and the alpha by a forward slash (/):

RGB syntax: red as a number 0-255 or percentage, space, green as a number 0-255 or percentage, space, blue as a number 0-255 or percentage, and optionally, forward slash alpha as a number 0-1 or percentage

/* old notation */
color: rgb(255, 255, 255);
color: rgba(255, 255, 255, 1);

/* new notation */
color: rgb(255 255 255);
color: rgb(255 255 255 / 1);

Another change is the consolidation of rgb and rgba into a single function (rgb) that will take the four arguments: the three colors and an optional alpha. The function rgba will remain but as legacy.

The color values can be a number from 0 to 255, or a percentage from 0% and 100% (both included). While the alpha value can be represented as a float number from 0.0 to 1.0, or as a percentage from 0% to 100%.

One important thing to take into account: you can use numbers or percentages but not combine them in the same rgb/rgba function.
This may seem obvious, but there is an edge case that may not be so obvious: the zero. The unit can be omitted in other properties if the value is 0, but that's not the case for the color functions: rgb(0%, 100%, 100%) is a valid color, while rgb(0, 100%, 100%) will fail.

Taking into consideration the old and new notations, rgb now supporting alpha, rgba kept as legacy, and the different number/percentage formats that can be used, there are 24 different ways of representing the same color in RGB:

/* cyan/aqua color */
color: rgb(0, 255, 255);
color: rgb(0, 255, 255, 1);
color: rgb(0, 255, 255, 100%);
color: rgb(0%, 100%, 100%);
color: rgb(0%, 100%, 100%, 1);
color: rgb(0%, 100%, 100%, 100%);
color: rgb(0 255 255);
color: rgb(0 255 255 / 1);
color: rgb(0 255 255 / 100%);
color: rgb(0% 100% 100%);
color: rgb(0% 100% 100% / 1);
color: rgb(0% 100% 100% / 100%);
color: rgba(0, 255, 255);
color: rgba(0%, 100%, 100%);
color: rgba(0%, 100%, 100%, 1);
color: rgba(0%, 100%, 100%, 100%);
color: rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 100%);
color: rgba(0 255 255);
color: rgba(0 255 255 / 1);
color: rgba(0 255 255 / 100%);
color: rgba(0% 100% 100%);
color: rgba(0% 100% 100% / 1);
color: rgba(0% 100% 100% / 100%);

All the browsers that were tested for this article supported all the notations of RGB and RGBA listed above... Except for Edge on Windows that only supported the comma-separated notation, rgb with 3 arguments and rgba with 4 arguments (the "classic" notation).

HEX

HEX is a variant of RGB in which the values for each parameter are in hexadecimal format. The syntax consists of a hash sign (#) followed by the hexadecimal values which will range from 00 (0 in decimal) to FF (255 in decimal):

Hexadecimal syntax: hash R R G G B B

In the past, there were only 3 hexadecimal parameters: one for red, another for green, and another for blue. A value of 00 would mean the complete absence of the color, while FF would indicate its complete presence. The higher the numbers, the lighter the color.

A fourth optional hexadecimal value can be provided to indicate alpha: 00 would be completely transparent, and FF completely opaque:

color: #FFFFFF;   /* white */
color: #000000;   /* black */
color: #FF0000;   /* red */
color: #00FF00;   /* bright green (lime, see named colors below) */
color: #0000ff;   /* blue */
color: #800000;   /* darker red */
color: #FF0000FF; /* red (opaque red) */
color: #FF000088; /* semitransparent red */
color: #FF000000; /* transparent (transparent red) */

Notice that CSS doesn't distinguish between upper- and lower-case for hexadecimal values, so you could write: #ff0000 or #FF0000 or #Ff0000 and they would all be the same. Just keep it consistent to have a cleaner, more maintainable code.

If each of the color parameters –and transparency, if present– has the same digits (e.g. #EE44FF), there is the possibility to use a shorthand version of the hexadecimal notation, just putting each digit once:

/* regular hex */
color: #336699;
color: #336699FF;

/* shorthand hex */
color: #369;
color: #369F;

This shorthand notation can only be used with a "small" subset of the colors: 4,096 of the 16,777,216 possible combinations (or 65,536 of the possible 4,294,967,296 combinations if we take into account alpha.)

Taking into consideration all the possible combinations, in the "best-case scenario", a color could be represented in up to 4 different ways with HEX:

color: #336699;
color: #369;
color: #336699FF;
color: #369F;

All the browsers that were tested for this article supported all the notations of HEX listed above... Except for Edge on Windows that only supported HEX values without the alpha.

HSL

HSL stands for Hue-Saturation-Lightness. In this format, the developer specifies three values:

  • Hue: an angle in the color circle/wheel (see below).
  • Saturation: the color's saturation/brightness level. A value of 100% indicates a fully-saturated bright color, while lower values will lead to fully unsaturated gray colors.
  • Lightness: the level of lightness of the color. Lower values will be darker and closer to black, higher values will be lighter and closer to white.

Color Circle (aka color wheel)

Description of HSL

Similarly to RGB, there are two versions of the function: hsl and hsla (hsl with alpha)... and just like RGB, hsl and hsla are now basically synonyms: hsla is considered legacy, and the function moving forwards should be hsl.

Also, the "traditional" comma-separated notation is now superseded by the space-separated notation:

HSL notation: hue space saturation space lightness spaces and, optionally, forward slash alpha

One great thing about HSL is that it can be easily combined with CSS variables and the calc() function to create basic theming capabilities with pure CSS:

Taking into account the different combinations of functions (hsl and hsla), value formats (number or degree, or number of percentage for the alpha), and separators (space or comma), there are 24 different ways of writing a color with HSL in CSS:

color: hsl(180, 100%, 50%); 
color: hsl(180, 100%, 50%, 1);  
color: hsl(180, 100%, 50%, 100%);   
color: hsl(180deg, 100%, 50%);  
color: hsl(180deg, 100%, 50%, 1);   
color: hsl(180deg, 100%, 50%, 100%);    
color: hsl(180 100% 50%);   
color: hsl(180 100% 50% / 1);   
color: hsl(180 100% 50% / 100%);    
color: hsl(180deg 100% 50%);    
color: hsl(180deg 100% 50% / 1);    
color: hsl(180deg 100% 50% / 100%); 
color: hsla(180, 100%, 50%);    
color: hsla(180, 100%, 50%, 1); 
color: hsla(180, 100%, 50%, 100%);  
color: hsla(180deg, 100%, 50%);
color: hsla(180deg, 100%, 50%, 1);  
color: hsla(180deg, 100%, 50%, 100%);   
color: hsla(180 100% 50%);  
color: hsla(180 100% 50% / 1);  
color: hsla(180 100% 50% / 100%);   
color: hsla(180deg 100% 50%);   
color: hsla(180deg 100% 50% / 1);
color: hsla(180deg 100% 50% / 100%);

Once again, all the tested browsers support hsl and hsla with both notations... except for Edge on Windows, that only supports the comma-separated notation, the function hsl with 3 arguments, and hsla with four.

Named Colors

Some common colors have named aliases to make them easier to use and remember. For example, you don't need to know that #FF0000 or rgb(255, 0, 0) represents the color red. You can directly use the name red to include that color in your CSS.

At the moment of writing this article, there are 148 named colors, some of which are repeated. For example, aqua and cyan are equivalents to #00FFFF.

Some named color examples with their names and HEX and RGB equivalents

Some named colors with their names and HEX and RGB equivalents

All named colors are case-insensitive (including the special color keywords and system colors below), which means that it doesn't matter how the names are capitalized, they will be interpreted the same. For example, turquoise, Turquoise, or tUrQuOiSe will all represent the same color.

The list of named colors has changed over time, and it will change in the future with new additions –the last color added to the list was rebeccapurple (#663399)–, aliases, and removals.

Special Color Keywords

Apart from the color names, there are some other named "colors" and keywords that are worth mentioning:

transparent

There is a color that is not really a color but a complete lack of it. The keyword transparent is used as a shortcut for rgba(0, 0, 0, 0) (see RGB and RGBA above) and it represents a fully transparent color.

It doesn't matter much anymore as all modern browsers support the transparent keyword, but in the past, it was something to consider as some of them (notoriously Internet Explorer and early versions of the Android Browser) did not support it.

currentColor

It represents the current value of the color property. If none is specified, it is the inherited text color from the parent container. Taking that into account, the following CSS rules are equivalent:

.element {
  color: teal;
  background-color: currentColor;
}

.element {
  color: teal;
  background-color: teal;
}

The currentColor value is convenient when used with SVG. You can make the same SVG icon take different colors by specifying currentColor as the fill or stroke color.

inherit

inherit is a reserved word –not limited to colors– that indicates that the property takes the same computed value as the property for the element's parent.

For inherited properties, the main use would be to override another rule (as the value is already inherited from the parent).

System Colors

Finally, there are some other special color keywords. They match some system elements and are designed to keep consistency across the applications on the browser.

The system colors come in "pairs" of background-foreground colors. It is important to use the matching background/foreground color to avoid contrast issues.

Here is a table with all the system colors and their corresponding matches:

Background Color Foreground Color(s)
buttonFace buttonText
canvas activeText
canvasText
linkText
visitedText
field fieldText
highlight highlightText

There is one more system color that doesn't have a background match: grayText which is a color that has a lower contrast rate (although still readable) for disabled elements... and that, contrary to what the name may convey, it may not always be gray.

In the past, the list of system colors was considerably larger, but many of them have been deprecated. Still, browsers support many of those colors as legacy, but it may not be a good idea to use them.

This is a table with the different system colors organized by browser:

Table of system colors by browser displaying Chrome, Edge, Explorer, Firefox, Opera, Safari, and Tor

Only Chrome and the Chromium-based browsers support all the system colors. Edge/IE and Safari only support Button and Highlight. And Safari supports Button, Highlight, and Field (all with the foreground and background color).


Intermission

This ends the "classic" color formats. By that, I mean the ones that have been around for a while and that all browsers support (at least the comma-notation).

Now, we are going to review some new formats introduced as part of the CSS Color Module Level 4. They are a little less common, a little less supported –or not supported at all, to be exact (at least not at the moment of writing this post)–, and a little less known.

...but before all that, let's talk about an old curiosity from HTML4. A little silly color thing that is no longer part of the standard, but that many browsers still support for legacy reasons.

bgColor that allowed setting the background color of different tags and elements (e.g. <body> or <table>):

<body bgColor="skyblue">
  ...
</body>

The value of bgColor was supposed to be any color value as specified in this post, but in reality, it accepted anything. This means that any string would be considered and parsed... and brought us one of the most interesting Stack Overflow questions for HTML.

As explained in more detail in this 2004 post from Sam, this happens when browsers try to parse the string as a color (a behavior inherited from Netscape). The basic idea is that the string is broken in 3 equal-size parts, adding zeroes for missing or incorrect characters.

This graph shows a simplified version of how it works (refer to the blog post for more details):

Graph with how bgColor works

And here you can see it working (again don't do this):


Now that we had a little fun break with colors, we are going to review the new color formats and functions new to the CSS Color Module Level 4.


HWB

Warning: This color format is not widely supported at the moment. Be cautious when using it, and avoid relying on it in production environments.

HWB stands for Hue-Whiteness-Blackness, it is a color format close to HSL, but often seen as an easier option for humans to understand and work with.

The HWB parameters represent the following:

  • Hue: an angle in the color circle/wheel.
  • Whiteness: a percentage that represents the amount of white to mix into the color. The higher the value, the clearer/whiter the color.
  • Blackness: a percentage that represents the amount of black to mix in. The higher the value, the darker/blacker the color will be.

Description of how HWB is calculated with the color wheel and a triangle showcasing the different colors depending on the whiteness and darkness value from 0% to 100%

As this is a new color function, it automatically comes with the space-separated notation and there is no comma-separated notation (and not hwba). This is the only syntax to use:

HWB syntax: hue value in number or degrees, space, whiteness in percentage, space, blackness in percentage, and optionally, forward slash and alpha in number or percentage

If the values of white and black add up to 100% (after normalization), the color will be achromatic: a shade of gray without any hint of the original hue value.

This simple syntax allows for 6 different combinations to represent the same color:

color: hwb(180 0% 0%);
color: hwb(180 0% 0% / 1);  
color: hwb(180 0% 0% / 100%);
color: hwb(180deg 0% 0%);
color: hwb(180deg 0% 0% / 1);
color: hwb(180deg 0% 0% / 100%);

None of the tested browsers supported the hwb() function.

Lab

Warning: This color format is not widely supported at the moment. Be cautious when using it, and avoid relying on it in production environments.

Defined by the CIE in 1976 after human vision experiments, the CIELAB colorspace (sometimes written CIE L*a*b* or simply Lab) represents the range of colors that humans can see.

Lab colors are expressed using three values:

  • Lightness: from black to white. The lower the value, the darker it will be (closer to black).
  • a*: from green to red. Lower values are closer to green, while higher values are closer to red.
  • b*: from blue to yellow. Lower values represent blue, and moves to yellow the higher the value is.

The syntax of the Lab function is as follows:

Lab syntax: lightness expressed in percentage. A-axis, a number between -160 and 160. B-axis a number between -160 and 160. Optionally, an Alpha value in number or percentage.

The value for the lightness can be any percentage, not limited to 0% and 100%: if the value is lower than 0% (black), it will be clamped to 0%, but it can be higher than 100% (white), which would represent extra-bright whites on some systems.

The value for a* and b* can be any number, but it will generally go from -160 to 160. And finally, the optional alpha value that is common to every color function. All the parameters separated by spaces (new notation) except the alpha by a forward-slash.

Taking the possible values and formats, the same color could be written in three different ways in Lab:

color: lab(67.5345% -8.6911 -41.6019);
color: lab(67.5345% -8.6911 -41.6019 / 1);
color: lab(67.5345% -8.6911 -41.6019 / 100%);

At the moment of writing this post, none of the browsers supported the lab() function.

LCH

Warning: This color format is not widely supported at the moment. Be cautious when using it, and avoid relying on it in production environments.

LCH stands for Lightness-Chroma-Hue. It is related to Lab (it has the same L value), but instead of using the coordinates a* and b*, it uses the C (Chroma) and H (Hue).

Although LCH and HSL share two letters (Lightness and Hue), it is important to differentiate them and clarify that the L in HSL and the one from LCH do not match. Also, even when the Hue is interpreted similarly in HSL and LCH, the angles are not mapped in the same way.

This is the syntax for the lch() function:

LCH syntax

As mentioned above, the Lightness is similar to the one from Lab. The Chroma could take any numeric value, but in practice, the minimum useful value will be 0 and the maximum useful value will be 230.

The third argument is the hue angle that can be represented as a number or as an angle (in degrees), and optionally the fourth argument will be the alpha. This is a newly added function so its syntax will follow the space-separated notation (with a forward slash for the alpha).

A color can be written in six different ways using lch():

color: lch(67.5345% 42.5 258.2);
color: lch(67.5345% 42.5 258.2 / 1);
color: lch(67.5345% 42.5 258.2 / 100%);
color: lch(67.5345% 42.5 258.2deg);
color: lch(67.5345% 42.5 258.2deg / 1);
color: lch(67.5345% 42.5 258.2deg / 100%);

None of the tested browsers supported the lch() function for colors.

CMYK

Warning: This color format is not widely supported at the moment. Be cautious when using it, and avoid relying on it in production environments.

While screens typically display colors in RGB, other devices represent colors in different ways. For example, printers generally use combinations of cyan, magenta, yellow, and black to represent colors because those are the most common ink colors.

CMYK stands for Cyan, Magenta, Yellow, and Key, which matches the ink colors in the printer (with black for key). And it could be a good format to pick if the end goal is to have the content physically printed (as it will better match the mixture of colors).

The function to specify the CMYK format is not cmyk() as one could have expected but device-cmyk() because it defines a device-dependent CMYK color:

device-cmyk function syntax: cyan (number or percentage). Space. Magenta (number or percentage). Space. Yellow (number or percentage). Space. Black (number or percentage). Optionally, forward slash alpha (number or percentage). Optionally, comma and a fallback color in any format

As in previous color functions, as it is a newly released one, it only comes with the new space-separated notation. But cmyk() has something different from the other color functions: it allows adding an optional fallback color in case the specified CMYK color is not valid.

Considering the option of using a number or a percentage –remember, they cannot be combined, it's one or the other for all the arguments–, the inclusion of alpha and a fallback, there are 12 combinations for the same color in CMYK format:

color: device-cmyk(1 0 0 0);
color: device-cmyk(1 0 0 0 / 1);
color: device-cmyk(1 0 0 0 / 100%);
color: device-cmyk(1 0 0 0, #00FFFF);
color: device-cmyk(1 0 0 0 / 1, #00FFFF);
color: device-cmyk(1 0 0 0 / 100%, #00FFFF);
color: device-cmyk(100% 0% 0% 0%);
color: device-cmyk(100% 0% 0% 0% / 1);  
color: device-cmyk(100% 0% 0% 0% / 100%);   
color: device-cmyk(100% 0% 0% 0%, #00FFFF); 
color: device-cmyk(100% 0% 0% 0% / 1, #00FFFF); 
color: device-cmyk(100% 0% 0% 0% / 100%, #00FFFF);

As a curiosity, when selecting a value for a color input on a Mac computer, the menu will give us the option of picking the color on device-CMYK too:

CMYK color picker on Mac

The color() Function

Warning: This function is not widely supported at the moment. Be cautious when using it, and avoid relying on it in production environments.

The color() function allows defining colors in a particular colorspace. It will take as parameters a comma-separated list of colors (that can be from different colorspaces), with the last one (this time from any type) acting as a fallback.

Its syntax may seem complicated compared to the previous ones but, as we'll soon see, it is simpler than what it seems:

Color function syntax is simpler than what it seems

The arguments for the color() function are:

  1. Colorspace identifier: this is an optional string that will help identify the colorspace used in the function (see below for some predefined colorspaces). If none is provided, srgb is the default value.

  2. Followed by one of these:

    • One or more numeric values (they can be number or percentages) that will match the values for the parameters in the specified color space. Or,
    • A string with the name of the color as defined in the specified colorspace. Not all colors will have string names.
  3. Alpha value: we've seen this one in other color functions, it indicates the opacity of the color, it is an optional value that can be a number or a percentage.

  4. Fallback value: an optional color value in any of the formats available in CSS.

You may have noticed that brackets are surrounding from the colorspace to the alpha (steps 1-3), which means that we can have multiple colors that would be separated by commas.

Five predefined colorspaces can be used with the color() function. We are going to add a short description without getting into much detail for each of them:

  • a98-rgb: compatible with Adobe© 98 RGB (thus the name). It is a common colorspace for photography.
  • display-p3: supported by most TVs, modern displays, and computer/laptop screens, which can display all (or almost all) the display-p3 colors.
  • prophoto-rgb: often used in digital photography for the master version of the images.
  • rec2020: this colorspace is used by photographers and by the broadcast industry —where it is the standard— for High-Definition, 4K, and 8K television.
  • srgb: the default colorspace for CSS. And its result would be equivalent to using the rgb() function.

All these colorspaces take three parameters that will correspond to red, green, and blue respectively, which values can go from 0 to 1 or from 0% to 100%. Lower values imply a lack of that color, while full 1 or 100% values indicate the full application of the color.

Some examples of the color() function with different combinations of colorspaces, alphas, and fallbacks:

color: color(a98-rgb 0.44091 0.49971 0.37408);
color: color(display-p3 0.25 0.12 0.45);
color: color(prophoto-rgb 0.36589 0.41717 0.31333);
color: color(rec2020 0.534 0.123 0.121 / 1);
color: color(srgb 100% 0% 0%, #F00);
color: color(srgb 1 0 0, srgb 0.865 0.417 0.333, #F00);

Conclusion

Wow! That must be the longest article that I have written in a while. If you made it here, thanks for reading!

In the article, we have:

  • Seen the "classic" colors (HEX, RGB, HSL, and named colors)
  • Explored the newly defined functions (HWB, Lab, LCH, CMYC, and color())
  • Reviewed the new space-separated syntax for the function.
  • Checked how there are around 100 different ways of representing the same color in CSS (not counting the infinite possibility of fallbacks!)

And all that may lead people to ask a couple of questions:

Which color format should I use?

That is the million-dollar question, and there isn't a clear answer: some developers will say: "use the format that uses the least characters", some others will say "stick to HEX because it is more efficient" and many more will reply "but what about transparency?"...

Combining different formats just because they are shorter? That sounds like a bad idea as it would make the code less maintainable. And all for what? Saving a big total of 100 characters in a 20KB file?

Opting for a format that supports transparency/alpha and always having the value as 1? It seems a bit unnecessary. Ignoring the formats that support alpha and apply transparency via other methods? That could have nasty side effects.

Considering that most color functions now support alpha, the questions in the paragraph above may be outdated, but there are still browsers that may not support them.

The best recommendation would be: think about what you want to do with the color, and apply the format that is more convenient for your particular case.

What's next for CSS colors?

Many changes will happen in the CSS definition of colors. New functions will be added, more will be removed... and some will be added then removed before we are even able to use them (like the gray() function).

And the same thing with colors, properties, or arguments. As mentioned above, one of the beauties of the web standards is that they are alive and continuously changing to adapt and improve the experience.

We'll probably see new color functions popping up (hsv()?) even when they could be supported as part of the color() function. Although, let's go one step at a time, and hope for more support from the browsers.

Article originally published on