Skip to content

Instantly share code, notes, and snippets.

@Myndex
Last active June 16, 2023 02:59
Show Gist options
  • Save Myndex/e1025706436736166561d339fd667493 to your computer and use it in GitHub Desktop.
Save Myndex/e1025706436736166561d339fd667493 to your computer and use it in GitHub Desktop.
FLIP FOR COLOR — If you want your text to be either black or white if the user selects some random color, just where is that inflection point? Hint: It's NOT 18% Y.

Let's Flip For Color

Q: I want my text color to switch to black or white, depending on the background color the user chooses. Just where is that flip point?

Short Answer:

Calculate the luminance (Y) of the given color, and flip the text either black or white based on a pre-determined middle contrast figure. For a typical sRGB display, flip to white when Y < 0.36 (i.e. 36%)

Longer Answer

First I want to acknowledge the massive amount of misinformation on the internet on this particular subject. The fact this field is still a subject of active research and unsettled science adds to the fun. I came to this conclusion as the result of the last few years of research into a new contrast prediction method for readability.

The field of visual perception is dense and abstract, as well as developing, so it is common for misunderstandings to exist. For instance, HSV and HSL are not even close to perceptually accurate. For that you need a perceptually uniform model such as CIELAB or CIELUV or CIECAM02 etc. Some misunderstandings have even made their way into standards, such as the contrast part of WCAG 2 (1.4.3), which has been demonstrated as incorrect over much of its range.

Out in the wild, I see recommendations to apply .299, .587, & .114 to the red green and blue channels, but those values relate to an obsolete system. Some say (R+G+B)/3 which is also very wrong. Some methods are linear light light and don't follow perception. It's really a crazy wild west world of wacky wrong wonderings. Let's fix that.

First Fix:

The coefficients shown in many answers here are (.299, .587, .114) and are wrong, as they pertain to a long obsolete system known as NTSC YIQ, the analog broadcast system in North America some decades ago. While they may still be used in some YCC encoding specs for backwards compatibility, they should not be used in an sRGB context.

The coefficients for sRGB and Rec.709 (HDTV) are:

  • Red: 0.2126
  • Green: 0.7152
  • Blue: 0.0722

Other color spaces like Rec2020 or AdobeRGB use different coefficients, and it is important to use the correct coefficients for a given color space.

The coefficients can not be applied directly to 8 bit sRGB encoded image or color data. The encoded data must first be linearized, then the coefficients applied to find the luminance (light value) of the given pixel or color.

For sRGB there is a piecewise transform, but as we are only interested in the perceived lightness contrast to find the point to "flip" the text from black to white, we can take a shortcut via the simple gamma method.

Andy's Shortcut to Luminance & Lightness

Divide each sRGB color by 255.0, then raise to the power of 2.2, then multiply by the coefficients and sum them to find estimated luminance.

All code in this gist is plain vanilla Javascript.

 let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
          Math.pow(sG/255.0,2.2) * 0.7152 +
          Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4

Here, Y is the relative luminance from an sRGB monitor, on a 0.0 to 1.0 scale. This is not relative to perception though, and we need further transforms to fit our human visual perception of the relative lightness, and also of the perceived contrast.

The 38% Flip

But before we get there, if you are only looking for a basic point to flip the text from black to white or vice versa, the cheat is to use the Y we just derived, and make the flip point about Y = 0.38;. so for colors higher than 0.38 Y, make the text black #000 and for colors darker than 0.38 Y, make the text white #fff.

  let textColor = (Ys < 0.38) ? "#fff" : "#000"; // Low budget down and dirty text flipper.

Why 38% and not 50%? Our human perception of lightness/darkness and of contrast is not linear. For a self illuminated display, it so happens that 0.38 Y is about middle contrast for TEXT under averaged, typical conditions.

Yes it varies, and yes this is an over simplification. But if you are flipping text black or white, the simple answer can be a useful one.

Perceptual Bonus Round

Predicting the perception of a given color and lightness is still a subject of active research, and not entirely settled science. The L* (Lstar) of CIELAB or LUV has been used to predict perceptual lightness, and even to predict perceived contrast. However, L* works well for surface colors in a very defined/controlled environment, and does not work as well for self illuminated displays.

While this varies depending on not only the display type and calibration, but also your environment and the overall page content, if you take the Y from above, and raise it by around ^0.678 to ^0.72, you'll find that the resultant 0.5 is typically the middle point to flip the text from white to black.

  let textColor = (Math.pow(Ys,0.678) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.

Using the exponent 0.6 will make the text color swap on a darker color, and using 0.8 will make the text swap on a lighter color.

Spatial Frequency Double Bonus Round

It is useful to note that contrast is NOT just the distance between two colors. Spatial frequency, in other words font weight and size, are also CRITICAL factors that cannot be ignored.

That said, you may find that when colors are in the midrange, that you'd want to increase the size and or weight of the font.

  let textSize = "16px";
  let textWeight = "normal"; 
  let Ls = Math.pow(Ys,0.678);

  if (Ls > 0.35 && Ls < 0.65) {
      textSize = "18px";
      textWeight = "bold";
      }  // scale up fonts for the lower contrast mid luminances.

Hue R U

It's outside the scope of this post to delve deeply, but above we are ignoring hue and chroma. Hue and chroma do have an effect, such as Helmholtz Kohlrausch, and the simpler luminance calculations above do not always predict intensity due to saturated hues.

To predict these more subtle aspects of perception, a complete appearance model is needed. R. Hunt, M. Fairshild, E. Burns are a few authors worth looking into if you want to plummet down the rabbit hole of human visual perception...

For this narrow purpose, we could re-weight the coefficients slightly, knowing that green makes up the majority of of luminance, and pure blue and pure red should always be the darkest of two colors. What tends to happen using the standard coefficients, is middle colors with a lot of blue or red may flip to black at a lower than ideal luminance, and colors with a high green component may do the opposite.

That said, I find this is best addressed by increasing font size and weight in the middle colors.

Putting it all together

So we'll assume you'll send this function a hex string, and it will return a style string that can be sent to a particular HTML element.

See my GitHub repo Fancy Font Flipping

And the live page demo here

Or play with play around with it at:

One of the things the sample code does is increase the text size for the lower contrast midrange. Here's a sample:

samples

And if you want to play around with some of these concepts, see the SAPC development site at https://www.myndex.com/SAPC/ clicking on "research mode" provides interactive experiments to demonstrate these concepts.

Andrew Somers
Senior Color Science Researcher
Myndex Perception Research

Terms of enlightenment

  • LUMINANCE: Y (relative) or L (absolute cd/m2) a spectrally weighted but otherwise linear measure of light. Not to be confused with "Luminosity".

  • LUMINOSITY: light over time, useful in astronomy.

  • LIGHTNESS: L* (Lstar) perceptual lightness as defined by the CIE. Some models have a related lightness J*.

  • CONTRAST: As used on this page, mainly it means the perceived difference between two colors displayed on a computer monitor. However, it's useful to point out that “Contrast” can be used to describe the differences of other perceptions: Contrast of size, contrast of position, contrast of speed, etc.

  • PERCEPTION: As it happens, perception is very much a neurological function. That is, it happens in our brain. We perceive things a certain way, but what we perceive is essentially representational and not an absolute quantity of what we're seeing, feeling, or hearing. The important understanding here, is that what we perceive is subject to our neurological system's interpretation of the physical stimuli. This is where the term “psychophysics” comes from. Partly to do with stimulus in the physical world, and partly to do with how our brain interprets it.

  • POLARITY: In this context Polarity applies to the values of a background and the stimuli. In other words, is it dark text on a light background, or light text on a dark background.

  • BoW and/or NORMAL mode: means dark text on a light background (normal mode). Confusingly some researchers call this negative contrast, others call it positive display. I'm pushing to move toward “NORMAL” to describe black print on white (or maybe "paper mode") and then “REVERSE” to describe light text on a dark BG, which is how people often describe that.

  • WoB and/or REVERSE mode: means light text on a dark background (reverse mode).

  • WCAG: Web Content Accessibility Guidelines. In the context used here, I refer to the math used for the 1.4.3/1.4.6 contrast assessments, which is essentially just a simple ratio of luminance between two sRGB colors with a small offset.

  • SAPC: stands for “S-LUV Advanced Predictive Color”, ... S-LUV, a Luv* type colorspace designed specifically for accessibility and self illuminated displays.

  • APCA: Advanced Perceptual Contrast Algorithim. The method for assessing supra threshold contrast on a monitor in a perceptually uniform way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment