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()
andrgba()
... but did you know that those functions are synonyms? So you can dorgb(255, 0, 0, 1)
orrgba(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 (/
):
/* 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, whilergb(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):
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.
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:
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
.
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:
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.
Fun with HTML4
Note:
bgColor
attribute is deprecated and should NOT be used. This section is just for fun and to show what's not to do.
In previous versions of HTML, there used to be an attribute and property called 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):
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.
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:
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:
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:
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:
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:
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:
The arguments for the color()
function are:
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.-
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.
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.
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.
Top comments (3)
This article is pure gold! It inspired me to revisit my year old "validate-color" npm package (one year tomorrow) and add support for the remaining colors, being proposed in the Module 4 Editor's Draft. Today I added support for both "HWB" & "Lab", while making important changes to other validations. The other colors are coming soon!
The validator is here: github.com/dreamyguy/validate-color.
Thanks again for the article!
Grreat article. What is the browser support like now days for using the forward slash, "/", for the specifing alpha channel for rgb and hsl? In your opinion, is it generally safe to completelhy drop the rgba and hsla format?
If I recall correctly, the support for the new notation is extended. IE was the only one that gave some issues, but now it's gone.