Skip to main content

Designing a better terminal text color experience

Hello, it is 2019. We've been computing together for over 50 years. And then there's this:


Text in the ANSI 16 color palette that, for some strange reason, is a thing that still exists.

Okay, I get it. Most of us who use the terminal (aka console, Command Prompt, whatever you prefer to call it) are down to earth, get the job done software developers and system administrators and not graphics design artists. But isn't that just a teesy, tiny bit painful to look at? And isn't this almost 2020? Many of the text bits are quite unreadable - the black text on black background in the above image is especially invisible. And the colors you can see for the most part just yell, "I'm a color! Look at me for an extended period of time and get a free headache!"

Sure, each and every user can usually change default colors to something else but these are the default colors.

Actually, it is worse than that, excluding the usually but not always configurable 16 color palette, here are the remaining unconfigurable 240 colors from the default 256 color palette on a black background for any XTerm-compatible terminal:

A good number of the colors in that palette produce text that is unreadable on a black background for several different reasons. And this is where I began the adventure to possibly discover a better terminal text color experience. It involved some programming, some math, and staring for hours on end at lots of colors.

But before I go too far into the weeds, if you only want fancy-shmancy lists of colors and code and links to tools/resources, jump straight to the best part. You'll miss all the good stuff though. But I understand - you're busy and don't need to learn anything new.

SEIZURE ALERT: There are no animations but some of the extreme color combinations shown here in this post may trigger or induce seizures, headaches, or migraines. Proceed with extreme caution.

The two main questions I want to answer by the end of this post are:

  • What are the most readable text colors on a black background?
  • Can we programmatically determine what the most readable text colors are for any given background color?

Let's start by looking at my favorite color picker, which happens to be the color picker found in Photoshop:

The default mode of this color picker works naturally with how we think about color. If I were to ask you to show me a blue, what would you do? Well, the natural thing to do is to find blue in the vertical Hue bar and probably put Saturation and Brightness to the maximum value possible (HSB is a term we'll see again in a bit). In programmer's terms, it would probably be RGB hex color #0000FF - that is, Red = 0, Green = 0, Blue = Max value (255). Maximum blue #0000FF looks like this on black:


Spicy jalapeno pastrami flank sirloin strip steak.
Turducken boudin buffalo picanha tenderloin.
Filet mignon buffalo pork loin andouille.

Oof. That's a little painful to look at. But we made the text blue...right? Well, sort of. The computer perfectly followed all of the instructions it was told to follow but the human eye doesn't work the same way. If we put that same color on a white background:


Spicy jalapeno pastrami flank sirloin strip steak.
Turducken boudin buffalo picanha tenderloin.
Filet mignon buffalo pork loin andouille.

We can see that the blue text is much more readable. Well, what if we just cut down on having so much blue?


#0000FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#0000EE:  Spicy jalapeno pastrami flank sirloin strip steak.
#0000DD:  Spicy jalapeno pastrami flank sirloin strip steak.
#0000CC:  Spicy jalapeno pastrami flank sirloin strip steak.
#0000BB:  Spicy jalapeno pastrami flank sirloin strip steak.
#0000AA:  Spicy jalapeno pastrami flank sirloin strip steak.
#000099:  Spicy jalapeno pastrami flank sirloin strip steak.
#000088:  Spicy jalapeno pastrami flank sirloin strip steak.
#000077:  Spicy jalapeno pastrami flank sirloin strip steak.
#000066:  Spicy jalapeno pastrami flank sirloin strip steak.
#000055:  Spicy jalapeno pastrami flank sirloin strip steak.
#000044:  Spicy jalapeno pastrami flank sirloin strip steak.
#000033:  Spicy jalapeno pastrami flank sirloin strip steak.
#000022:  Spicy jalapeno pastrami flank sirloin strip steak.
#000011:  Spicy jalapeno pastrami flank sirloin strip steak.

Nope. None of those blues are particularly readable on black. But you might say, "Now wait a minute, I can read those!" Okay, fine, let's demonstrate the exact same problem with two different colors - yellow text on a white background:


Spicy jalapeno pastrami flank sirloin strip steak.
Turducken boudin buffalo picanha tenderloin.
Filet mignon buffalo pork loin andouille.

The specific problem I'm addressing here is eyestrain. Sure I can read that blue text on a black background and even yellow text on a white background to a limited extent, but it's not comfortable to do so for any length of time. For those of us who sit in front of a computer screen for hours on end like many people do in this day and age, eyestrain can lead to headaches, nausea, blurred vision, and has long-term health and quality of life impact. These are well-known and scientifically proven outcomes. But what can we do about it?

So far we've only looked at hue (blue) and brightness (color vs. black). The fact that adjusting brightness didn't really help only partially explains why lowering brightness didn't work here. The one attribute we haven't looked at yet is saturation. Saturation is how light or intense/saturated a specific hue is. So let's crank brightness back up to 100% and only adjust saturation:


#0000FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#1A1AFF:  Spicy jalapeno pastrami flank sirloin strip steak.
#3333FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#4D4DFF:  Spicy jalapeno pastrami flank sirloin strip steak.
#6666FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#8080FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#9999FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#B3B3FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#CCCCFF:  Spicy jalapeno pastrami flank sirloin strip steak.
#E5E5FF:  Spicy jalapeno pastrami flank sirloin strip steak.
#FFFFFF:  Spicy jalapeno pastrami flank sirloin strip steak.

Other than the last entry, which is white, all of the others are the exact same blue-ish hue. Somewhere around the #4D4DFF and the #6666FF area is where the text still has a clearly blue-ish hue but is also clearly readable. At this point you might be thinking of a word like "contrast". Indeed, we've achieved contrast between the blue text and the black background. However, we've discovered something else: The original blue text on a black background is incompatible just as yellow text on a white background is incompatible. In fact, many color combinations are incompatible with each other.

If the end goal is to produce an algorithm to figure out the nearest compatible colors for foreground and background color inputs, then we're going to need to first figure out what is incompatible. Fortunately, back around the 1920's a couple of studies were conducted that get us halfway there. The results of the studies ultimately created a set of mathematical formulae that convert colors into a Luminescence value and two chromatic 'a' and 'b' components - aka Lab or CIE-Lab (1976). Microsoft and a few other players many years later, when the need arose, furthered the work for Standard RGB (sRGB) conversions to CIE-Lab (and back again), which is useful for anyone doing color space work on a modern computer screen. This is a very long rabbit trail you can choose to follow on your own time. The main component of CIE-Lab that we are after in this case is the L component, or Luminescence. Here's another Photoshop color picker screenshot but this time using Luminescence mode:

This is quite interesting because we can see that for minimum Luminescence, black and the color blue are there but red also shows up. So let's see what red text on a black background looks like:


#FF0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#EE0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#DD0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#CC0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#BB0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#AA0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#990000:  Spicy jalapeno pastrami flank sirloin strip steak.
#880000:  Spicy jalapeno pastrami flank sirloin strip steak.
#770000:  Spicy jalapeno pastrami flank sirloin strip steak.
#660000:  Spicy jalapeno pastrami flank sirloin strip steak.
#550000:  Spicy jalapeno pastrami flank sirloin strip steak.
#440000:  Spicy jalapeno pastrami flank sirloin strip steak.
#330000:  Spicy jalapeno pastrami flank sirloin strip steak.
#220000:  Spicy jalapeno pastrami flank sirloin strip steak.
#110000:  Spicy jalapeno pastrami flank sirloin strip steak.

Sure enough, that also hurts to look at for any length of time and shows that red is not really compatible with black either. However, changing just the saturation of red:


#FF0000:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF1A1A:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF3333:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF4D4D:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF6666:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF8080:  Spicy jalapeno pastrami flank sirloin strip steak.
#FF9999:  Spicy jalapeno pastrami flank sirloin strip steak.
#FFB3B3:  Spicy jalapeno pastrami flank sirloin strip steak.
#FFCCCC:  Spicy jalapeno pastrami flank sirloin strip steak.
#FFE5E5:  Spicy jalapeno pastrami flank sirloin strip steak.
#FFFFFF:  Spicy jalapeno pastrami flank sirloin strip steak.

Has a similar impact to changing saturation for blue. This might be useful.

Let's look at the other end of the spectrum for a moment. Here's another Photoshop color picker screenshot but with maximum Luminescence:

We can see here that yellow is among the same colors as white is. It can be surmised from the color picker images that colors of the same or nearby Luminescence value are fundamentally incompatible with each other.

Side note: The Photoshop CIE-Lab implementation is bizarre. I don't know how Adobe calculates it because their numbers don't match any known publicly published software-ready formula. What matters here is not getting Photoshop's numbers but to just visually see what is going on and get a feel for how CIE-Lab works from a layman's perspective without having to go too in-depth into color theory.

To test this theory, let's see if red and blue are severely incompatible with each other.


Spicy jalapeno pastrami flank sirloin strip steak.
Turducken boudin buffalo picanha tenderloin.
Filet mignon buffalo pork loin andouille.


Spicy jalapeno pastrami flank sirloin strip steak.
Turducken boudin buffalo picanha tenderloin.
Filet mignon buffalo pork loin andouille.

Yeah, so that's painful to look at.

However, we are definitely onto something here. What we need now are pleasing colors that don't instantly cause a migraine or eyestrain in general. We've discovered, on a black background, that maximizing brightness and reducing the saturation helps to increase the contrast while, at the same time, maintaining some color. We've also discovered that there is a set of incompatible colors. We've also discovered that, for very dark backgrounds, there needs to be sufficient brightness to avoid having the text disappear into the background altogether.

CIE-Lab effectively provides a convenient way to determine the perceptual color distance between any two colors to determine their compatibility. Sort of. It can be used eliminate extremely bad combinations but, by itself, isn't the perfect metric for calculating readability. After a lot of testing various thresholds, I ended up somewhere around a minimum Luminescence distance (L distance) of 41, a minimum Lab distance (aka Delta E) of 41, and a minimum average distance between the L distance and the Lab distance of 50. Those thresholds seemed to cut out most of the truly unreadable text color options across multiple background color tests. These thresholds don't really deal with oversaturated colors on dark/black backgrounds though.

To deal with oversaturated colors, we need to be able to push the color to a less-saturated point dependent upon the brightness of the background color.

In the image above, I've added a circle with a centerpoint roughly at 200% saturation/brightness. At a specific distance from the centerpoint, the saturation is at its maximum limit of compatibility with the background brightness (i.e. a circle where the edge represents the maximum allowable saturation). Also, certain colors are more easily oversaturated - in particular, yellow (hue 60) and blue (hue 240) - so the circle is a bit bigger for those hues to offset the extra saturation. As the brightness of the background increases, the maximum saturation circle shrinks as well until, at some point, it is gone.

Between CIE-Lab L and Delta E distances, maximum saturation, and minimum brightness calculations, it becomes possible to calculate, mathematically, readable foreground text colors for any given background color.

Before I get to the code in a moment, I want to clarify what "readable text" means to me:

  • Each target hue, brightness, and saturation of text is as close as possible to what is requested.
  • Text in the color in the Courier New font at 15 screen pixels tall is plainly readable from 3 1/2 feet away from the screen. Courier New is a precise, narrow 1 pixel wide font and also happens to be the default font for the Windows Command Prompt.
  • Selected text color has sufficient contrast from the background color. L distance, Lab distance, and average between L distance and Lab distance are far enough apart from each other.
  • Selected text color is not oversaturated on dark backgrounds. Oversaturated text causes a glow/halo effect that increases eyestrain.
  • Selected text color has sufficient brightness on dark backgrounds. Dark text on dark backgrounds blend into the background and increases eyestrain.

Source: PHP Miscellaneous Classes | ColorTools

In code above, there are three main functions: ColorTools::GetReadableTextForegroundColors(), ColorTools::FindNearestPaletteColorIndex(), and ColorTools::FindNearestReadableTextColor().

ColorTools::GetReadableTextForegroundColors() takes an input palette, such as XTerm::GetDefaultColorPalette(), and a background color and then decides if each color is a readable color for the background color. The resulting output palette only includes readable colors from the input palette.

ColorTools::FindNearestPaletteColorIndex() takes a palette of readable colors and finds the closest palette match for the input color.

ColorTools::FindNearestReadableTextColor() is a true color matching algorithm that performs hue, saturation, brightness sampling with the other two functions to find the closest color by building and processing one or more palettes.

Using the above functions, it becomes possible to map default palettes such as the default XTerm palette and ANSI named colors such that, no matter the background color, the foreground text colors are always readable:

On a completely black background, the after palette contains colors that are less saturated and quite readable. It's interesting to note that the colors that this algorithm chose for the ANSI named colors are quite similar to the Tango terminal theme used as the default theme for the Ubuntu desktop terminal:

While my primary focus was text on dark backgrounds, especially black, I wanted to make sure the algorithm works for other colors, especially a white background. So here's the algorithm's readable text output on a white background:

On a completely white background, there are fewer colors to choose from - mostly reds and blues and greens, some purples and dark oranges and muddy orange-yellows. Again, every single choice is quite readable and fairly comfortable to look at.

The next two are a bit strange: An ugly yellow and kind of a purple-ish blue. Two colors no one will likely have as a background color choice. Pre-emptively, I'll just point out that the results aren't perfect but I don't think they are terrible either given the background colors. If you feel you can do a better job, the source code is available to play around with.

This yellow-ish background color (#CDCD47) is pretty bad. Huge swaths of the before palette in this one are basically unusable, which results in a limited after palette and even it has some issues. The after palette 21, 57, and 92 indexes have a slight glow along the edges of the font, meaning they probably shouldn't be so saturated. Case in point: The ANSI named color "Bright Blue" is more readable than the "Blue". Regardless, that is one ugly looking yellow background. Let's move on...

This blue-ish/purple-ish background color (#6B6BED) is so bad that the algorithm decides that maybe a half-dozen colors in the original palette are readable. However, it could be argued that the 3rd from the last row in the original palette is fairly readable and that the black in the after palette isn't as readable as the white. Just goes to show that the algorithm isn't flawless. The goal wasn't really something "perfect" (also, the definition of "perfect" is different for everyone) but rather to go from "terrible colors => migraine" to "well, it's not great but at least I don't have a headache". The ANSI named color section here shows the dramatic transformation. Sure each color on the left is mostly a lost cause, but the text on the right side is certainly readable. So, overall, the general objective was achieved even if there are nitpicky little problems here and there.

Now that we've covered limited 256 color palettes, let's move onto true color. That is, the whole 16.7 million color spectrum a computer monitor can display (256 reds * 256 greens * 256 blues = 16.7 million colors). This is where things get fun and weird. While the entire spectrum is theoretically available, the ColorTools::FindNearestReadableTextColor() function actually performs color sampling and generates palettes across hue planes and lets the other functions do the heavy lifting. It'll sample up to 7,885 different colors (think of it as a very large palette) to find the closest color that qualifies as readable text. It tries to sample as little as possible and carefully fall back to more expensive samplings all for performance reasons. This approach allows background colors that are close to black and white to return results fairly quickly while more difficult background colors take longer to find foreground text colors. Even the slowest color calculations still happen pretty quickly but it is a noticeable slowdown.

On a black background, the algorithm is almost guaranteed to pick certain colors. The grayscale section is interesting and shows a minor flaw in the true color sampling approach. There's a subtle banding effect where brightness decreases slightly and then increases again. The reality is that no one would likely notice it except in this particular instance. On the plus side, there is at least one more orange variant and yellow-green color due to the decreased saturation.

This is probably my favorite transformation so far. The yellow, green, and light blue all stay roughly the same hue but have maximum saturation and differing brightness levels. The grayscale changes are quite nice as well. While my main objective was calculating readable text colors on a black background, I did pay some attention to white backgrounds but this transformation is a happy side-effect of the algorithm.

This middle gray background color (#808080) is really bad. I don't think I've ever seen anyone figure out what to do here with either this gray (#808080) or #7F7F7F. This is right at the mid-point between black and white where various algorithms I've seen decide to flip from a white foreground text color to a black foreground text color and where text basically vanishes. So if humans haven't figured it out, it isn't a huge shock that a computer can't either. Every color in the left column is unreadable to some extent (either oversaturated, faded out, etc - a case could be made for a couple of the colors like black and maybe the greens). I think the most difficult part to get past is the bright yellow/greens in the right column but everything in the right column is more readable in many cases. So while there's certainly room for improvement, the algorithm still manages to do an okay job of achieving the goal of finding readable text colors.

All of the screenshots for this article were put together using the "test_colors.php" tool from the PHP Miscellaneous Classes repository over on GitHub. It's a pretty easy tool to use. Just run "php test_colors.php" from a command-line to get started (requires PHP). Note that you must use a true color terminal for the tool to work properly - otherwise you'll get some really strange results. I primarily used the Windows 10 Command Prompt but I did test it under other systems as well.

Where to next? Well, terminals could be modified with a variation of this algorithm so that software running inside the terminal that emits unreadable text colors onto whatever the user chose as their default background color still results in readable text. The terminal would only apply the algorithm when the default background color is used for a character. This would still allow software to set both the foreground and background colors as they are able to right now and only affects default backgrounds. Also, terminal configuration tools could use the algorithm to warn about incompatible foreground color combinations for the ANSI named colors.

Other thoughts: I also don't think the algorithm is perfect. The tools are there to play with various numbers and thresholds and ideas. Feel free to mess around. I only spent about three weeks getting the algorithm to what you see here so there's obviously room for improvement. Also, you'll never look at color the same way again. Color choices for the most random things like road signs will pop out the next time you see them. I certainly find it interesting that various popular color schemes such as Tango, Gruvbox, etc. all seem to come accidentally close to what this algorithm outputs, which means it's probably on the right track.

Comments