Let's Create a Terminal Color Scheme

Software developers hold strong opinions about the weirdest stuff. We over-obsess about tabs vs. spaces, emacs vs. vim, dark mode vs. light mode, customize our tools so they’re just right for our personal preferences. I’m as guilty as the next dev. I spend an unhealthy amount of time tweaking my command-line experience (and I see all you hundreds of weirdos who starred that GitHub repo, what’s up with that?).

If we met at a party and I’d ask you about your favourite terminal color scheme, you’d unironically have an answer to that question. I know because I’m the same. Good thing we’ll never have this conversation — people with a favourite terminal color scheme don’t get invited to parties unless they know how to talk about normal topics.

But hey, look around, there’s just so much gorgeous stuff out there. From the mellow Solarized, to the retro-style Gruvbox, the vivid Dracula, all the way to the classy RosĂ© Pine and the soothing Catppuccin, people honestly put a lot of thought and care into tweaking the appearance of their developer tools. What’s not to love about all those hand-crafted, artisanal colors, freshly squeezed from free range, grass-fed pigments waiting to make you 30% more productive over night? You can even go pro and pay for extra premium colors if that’s your thing.

Totally Legit Terminal
                 -/+:.          ham@gaia
                :++++.          OS: macOS Sonoma
               /+++/.           Kernel: 23.0.0
       .:-::- .+/:-``.::-       Uptime: 25d 4h 41m
    .:/++++++/::::/++++++/:`    Packages: 672
  .:///////////////////////:`   Shell: zsh 5.9
  ////////////////////////`     Resolution: 5142x3840
 -+++++++++++++++++++++++`      DE: Aqua
 /++++++++++++++++++++++/       WM: Quartz Compositor
 /sssssssssssssssssssssss.      WM Theme: Blue
 :ssssssssssssssssssssssss-     Cereals: Root Loops đŸ„Ł
  osssssssssssssssssssssssso/`  With milk: You bet!
  `syyyyyyyyyyyyyyyyyyyyyyyy+`  Font: Fira Code
   `ossssssssssssssssssssss/    Disk: 391G / 469G (88%)
     :ooooooooooooooooooo+.     CPU: Intel Core i7-1065G7
      `:+oo+/:-..-:/+o+/-       GPU: Intel Iris
                                RAM: 9889MiB / 15573MiB

All snark aside, I’m a sucker for great looking dev tools. Being able to chose your favourite color scheme from the many great options out there makes our admittedly often dull work just a tad more fun, doesn’t it?

I’ve long wanted to create my own custom terminal color scheme. It seemed simple enough. What could be so hard about picking a few colors after all? Turns out it really isn’t all that simple if you want your color scheme to look pleasant and be functional. Creating a good color scheme forces you to think deeply about a lot of aspects, including:

  • Legibility: You’re reading text all day. Tons of it. You certainly don’t want to squint every time you read, so you want to choose colors with a good contrast to ensure legibility. People with impaired vision might have special needs here, for example when they have trouble distinguishing red and green colors.
  • Semantics: Colors have meaning. Red is often used for errors or to hint at dangerous options, yellow is used for warnings, green for success.
  • ✹Vibe✹: The overall vibe plays a big role in choosing a color scheme. Do you like a soothing palette? Do you prefer vibrant, playful colors? Do you like it pale and mellow? Are you a serious type? Are you looking for a retro feeling? Do you prefer greenscreen hacker aesthetics? Cold or warm? Dark or light?

Coming up with my own color scheme was quite an adventure and took me much longer than I expected. I learned that it’s super easy to create ugly color schemes. Creating a scheme that looks pleasant and covers all the qualities of a good color scheme took much more work and a methodological approach. In this post, I’ll walk you through how my approach to building a custom color scheme, explain what I learned along the way and show you what I came up with. Buckle up, it’s gonna get nerdy.

16 Colors is All You Need

Most terminal color schemes follow a simple pattern, going back to classic 3-bit or 4-bit color schemes. Here are the rules:

  • You get a total of 16 colors (that’s 24 colors, a 4-bit scheme)
  • You’ve got 8 different colors, called black, red, green, yellow, blue, magenta, cyan, and white
  • The values you use for each of these colors is totally up to you. By default, terminal color schemes use a red-ish color for red, a blue-ish one for blue and so on. But you don’t have to do that if you want to be fancy and are ready to sacrifice the semantics of colors. Also, it’s up to you if you pick tones that are vibrant, pale, dark, or light.
  • You can define bright versions of each of these 8 colors, but again, you don’t have to. Some color schemes define lighter shades of the regular 8 colors for all the bright colors, some use the same colors for both, regular and bright versions, some only define different shades for black and white in the bright color mode.

This pattern is defined in the ANSI X3.64 standard (the original history is a little more convoluted, but we’ll leave it at that) and still used by modern terminal emulators who interpret ANSI escape codes.

A big benefit of sticking to the ANSI standard when designing a terminal color scheme is that most terminal emulators we use these days will simply understand these color schemes. Windows Terminal, Konsole, Gnome Terminal, Alacritty, Kitty, iTerm, xterm, they all allow you to configure a simple, ANSI-compliant scheme via their user interface or their configuration files.

In the earlier days of computing, where we could only display 16 colors in total, our color palette might have looked like this:

Name Regular Bright
Black #000000 #808080
Red #800000 #ff0000
Green #008000 #00ff00
Yellow #808000 #ffff00
Blue #000080 #0000ff
Magenta #800080 #ff00ff
Cyan #008080 #00ffff
White #c0c0c0 #ffffff

Today, we’re no longer restricted to 16 colors but can choose from a much wider spectrum supported by modern displays. We can go ahead and find just the right tone of red, green, yellow, or blue to use for our perfect color scheme. The big challenge: finding colors that work well together in the large sea of colors at our disposal. The most obvious way to create a color scheme would be to open a color picker, pick 16 colors we like and call it a day.

A color picker Creating a color scheme can be as simple as manually chosing nice colors from a color picker — if you’ve got a keen eye and a lot of time.

If you’ve got a keen eye for colors and a lot of time on your hand this might work well. For the rest of us, picking 16 colors at random might yield pretty awkward and inconsistent results. In order to create a color palette that follows a consistent scheme and emanates a certain ✹vibe✹ we might need a more methodological approach. Let’s dive into some <gasp> color theory to see what we can come up with.

Doing Math with Colors

Instead of picking 16 random colors by hand, we can use different color models and basic math to pick colors that fit into the categories defined by the ANSI standard. Let’s look at a few different models and see how we can use them to our advantage.

The RGB Model

In the RGB color model you define colors according to their red, green, and blue (hence RGB, you guessed it) components. You give each component a value between 0 (no color) and 255 (full color) to determine the intensity of that particular color component.

In CSS we can use the rgb(red, green, blue) notation to define a RGB color:

        Red     Green    Blue

     rgb(255,     100,      0)
          |        |        |
full red -+        |        |
                   |        |
a bit of green ----+        |
                            |
no blue --------------------+

A vibrant red is represented as rgb(255, 0, 0) (the “red” component is at full intensity, all other components are not present at all), rgb(0, 0, 255) gives you a nice blue, mixing red and blue gives you magenta rgb(255, 0, 255) .. We often use a hexadecimal notation for the same colors #ff0000 in the RGB model.

For the purposes of finding a set of 16 different and consistent colors, using the RGB model isn’t really all that useful. When composing a color scheme, we intuitively think about the overall look of the scheme: should it be dark or light, use vibrant or muted colors? We hardly think about the amount of red, green, or blue we’d like to use in our colors. We could certainly try to come up with a clever scheme to mix the different RGB color components to create a consistent color scheme but maybe there’s a different color model that better fits our needs. Turns out there is, and it’s called “HSL”.

The HSL Model

In the HSL color model you once more define colors by providing 3 different components. Unlike in the RGB model, these components are no longer the intensity of red, green, and blue colors, but rather the hue, saturation and lightness of the color. Let’s dissect these to get a better feeling what they mean.

The hue component is a value between 0 and 360. This component defines the angle on the color wheel, 0 being a red hue, 120 being a green hue, and so on. The color wheel below shows how these angles map to a certain hue:

Color Wheel The color wheel, taken from MDN

The saturation component takes a value between 0% and 100% where 0% represents a completely desaturated (“gray”) color, and 100% a fully saturated, vibrant color according to the hue we’ve chosen.

And finally, the lightness component determines how light or dark a color appears. Just like the saturation, it takes a value between 0% and 100%.

CSS allows us to declare colors according to the HSL model using the hsl(hue, saturation, lightness) notation:

        Hue  Saturation  Lightness

      hsl(20      100%       50%)
           |        |         |
a red hue -+        |         |
                    |         |
fully saturated ----+         |
                              |
with medium lightness --------+

Using HSL notation, a full vibrant red can be represented as hsl(0 100% 50%) . If we want to turn it a little brighter, we can simply crank up the “lightness” component and get hsl(0 100% 70%) . If we want to turn the color a little less vibrant, we reduce the saturation and get hsl(0 50% 70%) . If we want a slightly different hue that still qualifies as red, we can tweak the hue component a little: hsl(20 100% 50%) .

Unlike the RGB model, the HSL model makes it easy to modify colors in a desired direction by increasing or decreasing a certain component of the color. Pretty slick! With the HSL model at our hands, we can now go ahead and declare an ANSI-compliant color palette without ever opening a color picker.

Let’s assume we’re aiming for a color palette that’s super vibrant and rather bright. Our palette will consist of 4 gray base colors (black, bright black, white and bright white) and 12 colorful accent colors. Thanks to some simple maths and an easy algorithm we can cook up a nice color scheme as follows:

For the 4 base colors (the gray tones) we use:

  • Saturation: 0%, since we want them all to be a bleak, desaturated gray.
  • Hue: The hue component doesn’t really matter if we use a 0% saturation (since at this point there’s no hue present), so we can simply set it to 0 for all base colors (we could pick any value in fact, it simply doesn’t matter).
  • Lightness: To get 4 base colors with different lightness, we divide the entire lightness equally and apply the resulting value to each of our base colors.

This gets us our 4 base colors: black hsl(0 0% 0%) , bright black hsl(0 0% 33.34%) , white hsl(0 0% 66.67%) and bright white hsl(0 0% 100%)

Let’s do the 12 accent colors next. Remember, vibrant and somewhat bright is the goal we’re aiming for:

  • Saturation: We want something vibrant. 100% is the way to go, anything else is for cowards.
  • Lightness: We want regular and bright versions of our accent colors. Let’s give our regular colors a lightness of 50% and use 75% for the bright versions and see how that goes, shall we?
  • Hue: This is where we need some maths again. Remember, hue is defined by the angle on the color wheel, a value between 0 and 360. 0° and 360° are red, 180° is cyan. We want 6 colors: red, yellow, green, cyan, blue, and magenta. We simply start at 0° and increase our hues in equal 60° steps until we get to 300° which is a nice and bright magenta.

Using this approach, we get our accent colors: red hsl(0 100% 50%) , yellow hsl(60 100% 50%) , green hsl(120 100% 50%) , cyan hsl(180 100% 50%) , blue hsl(240 100% 50%) , and magenta hsl(300 100% 50%)

Everything put together gives us the following color scheme:

Name Regular Bright
Black hsl(0 0% 0%) hsl(0 0% 33.34%)
Red hsl(0 100 50%) hsl(0 100% 75%)
Green hsl(120 100 50%) hsl(120 100% 75%)
Yellow hsl(60 100 50%) hsl(60 100% 75%)
Blue hsl(240 100 50%) hsl(240 100% 75%)
Magenta hsl(300 100 50%) hsl(300 100% 75%)
Cyan hsl(180 100 50%) hsl(180 100% 75%)
White hsl(0 0% 66.67%) hsl(0 0% 100%)

Not too shabby, but also not overly exciting. By picking fully saturated colors that sit somewhere halfway on the lightness scale, we get a palette that looks like it’s coming straight out of Windows 95’s Paint. If that’s your jam that’s cool, I’m not here to judge, but personally I’d like something a little more interesting.

Good thing we didn’t pick all these color by hand but instead employed a simple algorithm to determine the colors. This approach allows us to modify the color scheme by playing around with the individual components of our colors. You see those two saturation and lightness sliders above? Go ahead and play around with them and see how the theme changes. Pretty cool, huh?

Playing around with those sliders demonstrates how well the HSL color model works to generate entire color schemes based on a few input parameters. Unfortunately, it’s not all sunshine and rainbows. If you watch closely you might spot one of the shortcomings of the HSL model: Sometimes colors with the same lightness and saturation look different in direct comparison. One looks much brighter than the other. Look at blue and yellow, for example: hsl(60 75% 50%)   hsl(240 75% 50%)

Same lightness, same saturation. The only difference is their hue. Yet yellow appears much brighter than blue, doesn’t it? What we’re seeing here is a well-known shortcoming of the HSL model. Simply put, HSL doesn’t consider how our eyes and brains perceive color. As a result, two colors with the same lightness in HSL can be perceived as having very different lightness by our mushy brains. Meh. It could’ve been so easy.

HSL’s shortcoming has a direct impact on our terminal color scheme. By using HSL and our mathematical approach to generating 16 colors, we’d end up with a color scheme that doesn’t look uniform to our human eyes. Yellow tones would stand out against a dark background while blue hues would kinda blend into the background and become unnoticeable. Not great for the legibility we were aiming for.

While HSL works great for determining a color scheme based on a simple mathematical approach, the perception weirdness is something we want to get rid of. Maybe there’s a different color model to help us?

Perceptually Uniform Color Spaces, Oklab and Oklch

The shortcomings of HSL and other color models have kept a lot of smart people busy. It turns out that color theory is a fascinatingly complex field of research that I never spent any time thinking about. I don’t want to dive too deep into the dull theory, mostly because I’m too stupid to grok and explain it properly. I did understand a few high-level things we can use to our advantage, though.

First up, I learned that a while ago smart people came up with a color model that takes human perception into account. It’s called the CIELAB color space. This color space should help create colors that appear equally bright and saturated to our human eye. Sweet!

A few years ago the Oklab color space came on the scene to improve on existing colors spaces. It takes inspiration from CIELAB and has the goal of making it easy to generate colors that have predictable perceived lightness, hue, and “chroma” (more on that in a bit). Oklab became widely available in CSS in 2023 which brought it to my attention and the attention to a lot of web developers around the world. To represent a color in Oklab, you can use three components:

  • lightness determines the perceived lightness
  • a determines how green/red a color is
  • b determines how blue/yellow a color is

Fun. Sounds like the return of the RGB model, right? Just as defining the amounts of red/green/blue wasn’t particularly useful for generating our color scheme, determining the a and b coordinates for Oklab isn’t useful for our purposes either. Luckily there’s another popular color model in the Oklab color space. It’s called Oklch, is also part of CSS and helps us generate perceptually uniform colors by providing three components: lightness, chroma, and hue. Hue and lightness are very similar to what we know from our HSL color model - only this time we’re talking about perceived lightness to combat HSL’s major shortcoming. Chroma determines the intensity of the color. I’m tempted to say it’s the same as “saturation” but it seems like it really isn’t the same, and if you say it is, academics in the color theory space will come and haunt you in the afterlife. So it’s kinda the intensity of a color, but maybe not quite. Makes sense? Yeah, I know.

To create a color in the Oklch model, we can write it using the oklch(l c h) notation and all modern browsers will understand what you mean:

      Lightness   Chroma   Hue

      oklch(65%    0.21    180)
             |       |      |
medium light-+       |      |
                     |      |
pretty intense ------+      |
                            |
with a teal hue ------------+

This will give us a nice, mellow teal color: oklch(65% 0.21 180) . Nifty! With its perceptual uniformity we now found a color model that improves on HSL’s shortcoming and has three equally useful parameters. Seems like everything we’ve wished for. Maybe we can just use Oklch instead of HSL to determine our terminal color scheme then?

Not so fast. There’s one quirk with the Oklch color model that’s raining on our parade. Remember that chroma parameter we talked about? The one that’s totally not the same as “saturation”? Yeah, that one’s a little awkward. While the lightness and hue parameters are super easy to work with (the former takes a value between 0% and 100%, the latter an angle between 0° and 360°), chroma’s maximum value depends on the hue you’ve chosen. In other words: if you have a magenta hue (say 320), your maximum chroma can be about 0.3 on a regular monitor. But if you’ve got a teal hue (about 200), your maximum chroma value that can be displayed on a regular display is at about 0.1. Ouch! There seem to be good reasons to do this, but these are reasons that once more go beyond what I understand. The lovely people at Evil Martians have done a fantastic job explaining the Oklch color model in more detail and even built an interactive Oklch color picker you can use to explore how this color model behaves.

Without a predictable and uniform chroma parameter, our algorithm to generate a terminal color scheme quickly falls apart. We can’t simply say that our color scheme uses a chroma value of 0.3 for all the accent colors, for example, simply because this might be out of range for some hues.

Luckily, there is one savior that can help us out of this pickle, and its name is Okhsl.

The Okhsl model, the love-child of Oklab and HSL

Björn Ottosson, the same person who introduced the Oklab color space, came back with a blog post introducing two new color models, Okhsv and Okhsl. The latter looks like it’s built exactly for our purposes. Okhsl combines the benefits of HSL and Oklab in one color model:

  1. We get perceptually uniform colors. Colors with similar parameters will appear similar in brightness and intensity.
  2. We get the mathematical simplicity of HSL - each parameter has a predictable range of values

Unlike Oklab and Oklch notations, Okhsl is not part of the CSS standard yet, but thanks to libraries like culori.js we can start using it today if we really want to. With the help of a color library, we can then define colors according to the Okhsl model just like we could with the HSL color model (only this time the “lightness” parameter means “perceived lightness”):

         Hue  Saturation  Lightness

    okhsl(20      100%       50%)
           |        |         |
a red hue -+        |         |
                    |         |
fully saturated ----+         |
                              |
with medium lightness --------+

Putting it all together

Now that we’ve finally found a proper color model to suit all our needs, we can start putting it all together and generate our color schemes. We can use the same approach we used with the HSL color model, but by switching over to the Okhsl model we get perceptual uniformity. Colors with a similar lightness will now appear equally bright to our human eye.

Demonstrating the magic of Okhsl goes beyond what I can pull off in this blog post — mostly since our browsers can’t interpret okhsl() color notation yet. But I’ve got something better for you: Over the last few weeks I built Root Loops, a small and somewhat silly tool that allows you to play around with the Okhsl color space to generate your very own, beautiful terminal color schemes. You can play with different inputs to drive lightness, saturation, and hue shift of the base and accent colors. If you like what you see, you can simply export the resulting color scheme and start using it in your terminal emulator of choice, or wherever else you want to use these colors.

Root Loops Preview

You can find the tool over on rootloops.sh. The source code is available on GitHub. You can check out the implementation and see that the calculation follows the same basic algorithm I outlined in this blog post.

Root Loops encapsulates everything I’ve learned about color theory and terminal color schemes in the last few weeks and I’ve had a blast building it. I’m planning to extend it a little more over the next few weeks to make it easier to export color schemes for the tools you love.

Go ahead, go wild and try it out. Create a color scheme you like and use it for whatever you want. And please don’t hesitate to let me know if you found it useful or if you’ve got any questions, comments, or ideas.