Sorting color palettes perceptually

Use the following procedure to sort your RGB color palette by perceptual brightness:

  1. Pick an RGB color from your palette. I'll use the color "raw sienna" for this example - RGB <206 141 56>, or #ce8d38.
  2. Apply gamma expansion on each component: Cout' := (Cin/255) ^ 2.2, E.g.
  3. R = 206, R' = (206/255)^2.2 = 0.625345,
  4. G = 141, G' = (141/255)^2.2 = 0.271578,
  5. B = 56, B' = (56/255)^2.2 = 0.035614
  6. Perform a weighted sum of the expanded components, where the weights are the Rec. 709 luma coefficients. L = 0.2126*R' + 0.7152*G' + 0.0722*B'. E.g.
  7. L = 0.2126*R' + 0.7152*G' + 0.0722*B'
  8. L = 0.2126*0.625345 + 0.7152*0.271578 + 0.0722*0.035614
  9. L = 0.132948 + 0.194233 + 0.002571
  10. L = 0.329752

Repeat the process for each color in your palette, writing down the value of L for each. Sort the list by each color's L value - lower values correspond to darker colors, and vice versa. Once complete, you've sorted your palette on perceptual brightness.

There's more to this than a math exercise. If you notice two of your colors are close together in the sorted list, keep those colors apart in your page designs, especially areas with textual content. Colors that are close together have low contrast, and are difficult to perceive by people with moderately low vision, or color deficiencies.

Of course there are many facets to making webpages accessible to everyone. Not all of these are easy to achieve. Improved color contrast, as you can see, requires a little math and patience. It's nonetheless absent in many pages around the web.

Hopefully the above instructions make it available to everyone.

Why does it matter that it's "perceptual"?

Sorting is an operation you perform on lists with 1 dimension - e.g., you can sort numbers and letters, but not colors and sounds. If we want to sort a list of multi-dimensional quantities like color, we have to reduce each entry to a single dimension.

The bad news: there are many ways to reduce something like an <R,G,B> color to a single value. Some ways are simple - e.g. average the color components, ala L = (R+G+B)/3. Others are more complex, like the procedure outlined above.

Put another way, we have to assign a "brightness" (or lightness, or luminance, or ...) to colors in order to sort them. Each different approach to calculating brightness is useful for different reasons. The calculation of brightness presented is based on a model of average human perception. That means there are no surprises - after sorting, the dark colors look dark, and vice versa. That's not always the case when you choose a brightness calculation.

Create a monochromatic palette

Starting at the last step above:

  1. Apply gamma compression on the L value: Cout' := Round(255 * (L ^ (1/2.2))), E.g.
  2. Cin = 0.132948 + 0.194233 + 0.002571 = 0.329752
  3. Cout = Round(255 * (0.329752 ^ (1/2.2))) = 154
  4. Convert the luminance value to the equivalent RGB gray in CSS:
  5. Luminance(154) = rgb(154,154,154) = #9a9a9a

Repeat for each color in your palette. Your perceptual (gray) palette will have the same number of colors as your original.

I like to use this naming scheme in my LESS:

@raw-sienna: #ce8d38;
@raw-sienna-luma: #9a9a9a;

© 2021 David Mann. All content provided on this site, code or otherwise, is licensed and/or copyright as specified below: