03/06 2013

Color and Web Browsers

This post is related to the Design Miami meetup on March 4, 2013. I did a quick talk about "Color and Computers", and Dimitry Chamy did a wonderful and exhaustive talk about using colors in design. Links to meetup materials are available, and you’re welcome to future meetups.

Web browsers take descriptions of colors from CSS declarations. Some of them are:

#F0F
12-bit RGB color, four bits per hexit1
#FF00FF
24-bit RGB color, four bits per hexit
rgb(255, 0, 255)
RGB color, decimals
rgb(100%, 0%, 100%)
RGB color, decimal percents
hsl(300, 100%, 50%)
HSL color, decimal degree for hue, and percents for saturation and lightness2

The written-out RGB and HSL declarations with decimal-encoded numbers don’t specify precision per se. The W3C recommendation for the CSS Color Module doesn’t either. If you wanted, you could jam negative numbers or decimal places in there:

Values outside the device gamut should be clipped or mapped into the gamut when the gamut is known: the red, green, and blue values must be changed to fall within the range supported by the device. […]

Example(s):

em { color: rgb(255,0,0) }       /* integer range 0 - 255 */
em { color: rgb(300,0,0) }       /* clipped to rgb(255,0,0) */
em { color: rgb(255,-10,0) }     /* clipped to rgb(255,0,0) */
em { color: rgb(110%, 0%, 0%) }  /* clipped to rgb(100%,0%,0%) */

Since the W3C doesn’t provide much guidance, we have to see how browsers implement colors.

Google Chrome, Apple Safari, and other WebKit Browsers

CSS parsing is done by WebCore, mostly in WebCore/css/CSSParser.cpp3. It calls CSSParser::parseColorParameters for RGB colors:

bool CSSParser::parseColorParameters(CSSParserValue* value, int* colorArray, bool parseAlpha)
{
    // …
    for (int i = 1; i < 3; i++) {
        // …
        colorArray[i] = colorIntFromValue(v);

RGB color components are converted to integers. HSL:

bool CSSParser::parseHSLParameters(CSSParserValue* value, double* colorArray, bool parseAlpha)
{
    // …
    colorArray[0] = (((static_cast(parsedDouble(v, ReleaseParsedCalcValue)) % 360) + 360) % 360) / 360.0;
    for (int i = 1; i < 3; i++) {
        // …
        colorArray[i] = max(0.0, min(100.0, parsedDouble(v, ReleaseParsedCalcValue))) / 100.0;

HSL colors are parsed as floating-point (the double type), but, in WebCore/platform/graphics/Color.cpp

RGBA32 makeRGBAFromHSLA(double hue, double saturation, double lightness, double alpha)
{
    // …
   
    return makeRGBA(static_cast<int>(calcHue(temp1, temp2, hue + 1.0 / 3.0) * scaleFactor), 
                    static_cast<int>(calcHue(temp1, temp2, hue) * scaleFactor),
                    static_cast<int>(calcHue(temp1, temp2, hue - 1.0 / 3.0) * scaleFactor),
                    static_cast<int>(alpha * scaleFactor));
}

We see that our carefully described, bespoke, artisanal, floating point colors are being converted down to 24-bit RGB (well, 32-bit RGBA).

Mozilla Firefox and other Mozilla Browsers

HSL conversion for CSS is done in layout/style/nsCSSParser.cpp:

bool CSSParserImpl::ParseHSLColor(nscolor& aColor,
                                  char aStop)
{
  float h, s, l;

  //… 

  if (ExpectSymbol(aStop, true)) {
    aColor = NS_HSL2RGB(h, s, l);
    return true;

HSL is converted into an nscolor instance. As implemented in gfx/src/nsColor.h, it’s a 32-bit RGBA thing, just like WebKit.

Conclusion

Storing more than 32 bits of color in your RGB/RGBA declarations is silly. If you use either of the 24-bit representations above (or the 12-bit representation, since it converts losslessly into 24-bit), you’ll get correct4 colors.

If you use HSL colors, you’ll lose some precision in conversion, but probably no worse than you’ll lose when your site gets displayed on a 18-bit or fewer display (such as in the iPhone 3GS and earlier).

Notes

  1. "hexit" is like a digit but hexadecimal instead of decimal. If you like contractions, "hexadecimal digit" becomes "hexit."
  2. The degrees component doesn’t have a glyph because it’s unintuitive to type: ⌥⇧8 on Mac OS X, Alt-0176 on Windows, ⌃⇧-UB0 on Ubuntu.
  3. I’m not pasting whole functions because C++ is ugly and verbose.
  4. As much as you can have correct colors on the web.