Opium.Fill — standardization of color schemes through the eyes of a programmer

Denis Elianovsky
15 min readMay 24, 2020

Hello. Let’s try to figure out how designers use colors in UI and how the whole thing can be standardized without surrounding designers with restrictions.

I’ll tell you about the scheme which is called Opium.Fill. There is nothing particularly extraordinary or unique in it (aside from maybe the name). All the ideas are pretty obvious. It is a set of basic principles mixed with the love to name everything.

I will provide some implementation examples in React JS (for developers) and in Figma (for designers). This approach isn’t limited to React or Figma, however, I’m just more familiar with them.

This system can also be implemented in conjunction with the principles outlined by Material Design.

The article is written mostly for front-end developers and a little bit for designers.

Table of contents

  1. What kinds of problems are we solving?
  2. The ideology of Opium.Fill
  3. Basic assumptions
  4. Section #1. Parent colors
  5. Section #2. Substitutions
  6. Section #3. Shifts
  7. Color inversion
  8. Use
  9. When it doesn’t make sense to use the system
  10. Objections
  11. Conclusion

1. What kinds of problems are we solving?

1.1. 50 shades of gray

Nearly everyone is familiar with this particular problem. Although it’s most often noticed when working with various shades of gray, the problem isn’t limited to this specific hue. It also happens with various other colors, such as blue. So where do you stick with which shade? Even designers are often confused by this question.

1.2. Designer plays around with colors

Sometimes a designer can start using an entirely new color simply because they’re tired of the old one (or maybe they just sneezed while using the eyedropper). There is nothing inherently wrong with this, and you shouldn’t necessarily blame the designer. But a problem can arise when the developer isn’t aware of these changes.

In such cases color variables in CSS can accumulate to a great deal. The developer can’t be sure whether it’s necessary to remove some old colors, or whether they are still being used somewhere in the project. The highest number I saw was 273 color-only variables.

A similar situation can occur in projects where the work is done through Agile and the design changes simultaneously with the development.

1.3. Dark modes and design branding

This problem is caused by the previous one. If the project has a giant mass of disordered variables, it can be incredibly difficult to start using new color schemes in the project.

Let’s say you’re working on a CRM platform. Your CRM needs to give the client an option to switch the platform’s color scheme to one that matches the client’s corporate colors. If you don’t have a clearly defined color scheme already, then this may be quite difficult to accomplish.

But what if you need a dark mode for the application? Then the suggestion “John, let’s take our 273 variables and arrange them into a coherent order” will lead John to break the registration form at the 10th step, have a fight with a developer from the neighboring department, and John will eventually go crazy after a week of frustration.

2. The ideology of Opium.Fill

2.1. Don’t distract your designer from work

The scheme can be used to “decipher” the design, but not to overload designers. The designer doesn’t even need to know about Opium.Fill’s existence in order for everything to go according to the system.

It’s better to wait until the main concept of the application is essentially finished. Only then can it be useful to point out to the designer if something doesn’t fit in the color scheme, and to clarify whether these parts can be adjusted. With this approach, in 9 times out of 10, designers say “Pff, that’s not even a problem, let’s change it” or “Oh, I actually messed up a bit here, thanks for pointing that out.”

2.2. You can define the color just looking at it

If you take a second to remember your high school years, you’ll remember that the structure of the periodic table of elements allowed scientists to predict the existence of elements that had yet to be discovered in nature. Empty cells were pre-allocated for these “undiscovered” parts of the table. We can apply the same principle to our color scheme. We’ll make a table and leave some of the cells blank. But based on the cells’ parameters, we will be able to understand what color should be there.

2.3. We try cover the majority of needs but not all of them

Our task is to optimize the biggest, most boring part of working with colors. When developing an application, you may come across a color that doesn’t quite fit perfectly into the structure. If this happens 1 time out of 100, you shouldn’t consider it a problem.

3. Basic assumptions

3.1. Each color has a pair

We believe that each color has two “embodiments”. The first is the bright, saturated version (conditionally Strong). The second is light, unsaturated, barely distinguishable (conditionally Weak). So, if we see blue, then in addition to it being blue, we must define it as either Strong or Weak.

3.2. Separate colors by functionality

Forget about “sky-blue”, “gold”, “jet-black” and the alike when you’re naming colors. Color’s name should reflect its functionality, not its hex. From now on, we will be working with the following names: Base, Faint, Accent, Complement, Critic, Warning, Success.

3.3. Three sections

Section #1 is the most important. Section #3 is the least important. Below I will describe each of these sections. This separation is necessary to spot more significant colors and make the other colors less distracting.

4. Section #1. Parent colors

4.1. Names

Back to our new color names: Base, Faint, Accent, Complement, Critic, Warning, Success. Let’s take a look at each one individually.

Base

This is black and white. Or colors that are subjectively similar enough to them. They serve as the base for text and background.

Faint

A secondary text or a grayish background is Faint. Black with some transparency (if it is perceived as gray) is also included in this group.

Accent

This would be the primary corporate color or a color that highlights the most important elements of the interface. For the sake of convenience, let’s define Accent colors in our scheme as shades of blue.

Complement

This is an optional accent color. Far from everyone uses it. Let’s take look at Airbnb — I would say that their Complement color is dark green:

In our scheme I will define the Complement color as purple. This is used in the design examples that will be shown a little further.

Critic

This color is used for highlighting errors and other extremely significant information. It is usually something red.

Warning

If you want to indicate that something important but not critical is happening, then you will need a Warning color. These are usually defined as a kind of yellowish-orange.

Success

Sometimes it can be enough to use the Accent color to highlight a successful action. But if your Accent color can be also interpreted as an error (like red) or if you need to use a new color for some other reason, then this is where Success comes in. More often it will turn out to be something greenish.

These are all 7 main colors. It’s not necessary to use all of them. Naturally, you can also supplement this set if you have serious reasons for doing so.

4.2. Color families

You’ve probably already noticed that when I talk about color, I don’t use specific wording, but rather lean towards terms like “shades of blue”, “something greenish”, “yellowish-orange”, and so on. This isn’t just for fun. Let me introduce to you the “Color Family” concept.

As I said above, we divide every color into pairs. We can’t just limit ourselves to an Accent and leave it at that. We also need to have an Accent Strong and an Accent Weak. These are always two color variations that form the family. Like two partners in human families.

The Base colors are already split into a group of two (black and white). Let’s split the rest into families:

5. Section #2. Substitutions

5.1. Context

Take a look at Bitbucket. The designer considered the blue color on the background to be too dark for the text. So the text color was lightened. Now all the text actually has a different value in hex despite appearing to look the same as background blue:

Every color has a specific context in which it is used. A color can be applied to the background, text, lines, icons. These are what we call contexts. In any of these cases, the color may need to be changed somehow to better suit the context. We call such changes a substitution.

Let’s make a separate place in the table for the substitutions. Each new line represents a context where we can add the substitutions for that particular context. By default, these cells are empty:

We added substitution cells for only the Strong colors. The Weak colors in section #2 show how that color looks on a dark (inversed) background. We won’t touch them now, but we’ll come back to them later.

If the substitution cell isn’t filled in, then the parent color is used for the text, icons and everything else.

We can only substitute colors of primitive design elements. This particular set of contexts is taken almost exactly from graphic editors that are frequently used by designers. There you can draw a rectangle (background), add a block of text, draw a line or add an unusual shape, such as a star (which we treat as an “icon”).

5.2. Fancy

It is a special kind of substitution: substitution to a gradient color. We call them Fancy, a sort of context for a “special occasion”. Fancy color is the same for every context. You can’t have separate gradients for text, icons, etc. (technically, you can, but it’s not worth the effort to make the table more complex for such rare cases). Let’s make a separate line for the Fancy context at the very bottom of the table:

Now we’re prepared for some subtle changes depending on the context, since we know for sure that designers make them. The empty cells give us some flexibility. In some cases substitutions have to be performed often, and in some less so. If section #2 remains nearly empty, this is actually quite normal (designers also try to reduce the number of colors in the long run).

6. Section #3. Shifts

Sometimes a color needs to be darkened or lightened a little. But in this case, it’s not because of the context, but rather because each color has two additional states: a “darker” one and a “lighter” one. Generally, designers use these states to signify a mouseover (hover) reaction. We call this type of color change Shift. You can make a color shift up (darken) or shift down (lighten).

The empty table now looks like this:

7. Color inversion

I intentionally put off this topic, because the use of inversion is rare and not everyone actually needs it. Sometimes you need to write something on a dark side-panel, but not all colors adapt well to this. Let’s look at an example of one of our designs:

There is some text on a blue panel, which is subjectively perceived as Faint. There are also some circles under the icons that I would also define as being in the Faint family.

If the panel were light, then both of these elements would most likely just be shades of gray. But the panel is dark. There are currently no matching colors in our table, but you can add them into the blank cells. For this exact reason you will need the Weak column from section #2.

For this layout, the adjusted table looks like this:

Please also note that the lines will now be drawn with a lighter Faint Strong color, which is reflected in the table. Gradients for the Accent color and shifts for the parent Faints have also been added (they will come in handy when drawing a search field from the previous screen).

It’s important not to confuse color inversion with the dark (night) mode. The dark mode has plenty of its own color nuances, so it’s better to create an entirely separate table for it.

8. Use

8.1. CSS, React

Let’s go ahead and draw this button

In pure CSS, the variables can be organized like this:

:root {  /* Parent colors */
--base-strong: #000;
--base-weak: #fff;
--faint-strong: #8994A6;
--faint-weak: #F6F8FB;
--accent-strong: #0070FF;
--accent-weak: #EBF4FF;
--complement-strong: #8889E2;
--complement-weak: #EEECFD;
--critic-strong: #F74545;
--critic-weak: #FDEDED;
--warning-strong: #F8AE4F;
--warning-weak: #FCEBCF;
--success-strong: #27AE60;
--success-weak: #DEF8E9;
/* Shifts in Parent colors */
--faint-strong-down: #A5ADBB;
--faint-weak-up: #ECEEF5;
}
/* Substitutions */
/* Add contexts as classes */
.back {
--faint-weak: rgba(255, 255, 255, 0.15);
}
.text {
--faint-weak: rgba(237, 241, 247, 0.5);
}
.line {
--faint-strong: #EDEFF2;
}
.icon {}.fancy {
--accent-strong: linear-gradient(132deg, #3F89EE, #5447FF);
/* Shifts */
--accent-strong-down: linear-gradient(132deg, #448FF3, #594CFF);
}
/* Draw the button */.button {
background: var(--accent-strong);
color: var(--base-weak);
/* Here just add the decoration */
}
.button:hover {
background: var(--accent-strong-down);
}

And then, to create the button in HTML, you will need to add this to the layout:

<button class="fancy button">
<div class="text">
Hello Button
</div>
</button>

There is no way to natively track a particular context in CSS, so you have to specify it manually through the classes. You can, of course, try to understand the context based on particular tags, but this would be an amateur decision.

Another way is to determine colors through data attributes:

<button
data-back-fancy
data-back-color="accent-strong"
data-text-color="base-weak"
class="button"
>
Hello Button
</button>

Then your CSS should have this:

[data-back-color="accent-strong"] {
background-color: #0070FF;
}
[data-back-fancy][data-back-color="accent-strong"] {
background-image: linear-gradient(132deg, #3F89EE, #5447FF);
}
[data-text-color="base-weak"] {
color: #fff;
}
/* And so on for all other colors */

In React, the code might look something like this:

class Button extends React.Component {
render() {
return (
<Box fill="accentStrong" fancy className="button">
<Font fill="baseWeak">Hello Button</Font>
</Box>
)
}
}
// Or a bit shorter
class Button extends React.Component {
render() {
return (
<Box.Accent fancy className="button">
<Font>Hello Button</Font>
</Box.Accent>
)
}
}
// Box and Font — are the base components for primitives, which we use to understand the particular context// In the second example, you don’t even have to specify Strong or Weak (and for text you don’t even need to specify any color)
// The table often allows components to pick colors automatically
// Hover color can also sometimes be calculated automatically
// I won’t explain how to implement base components in this article
// I think you can figure it out for yourself
// Besides, it's not even necessary for Opium.Fill
// Only one possible implementation method is shown

By using this scheme, you’re not bound to use CSS (since you’re not only making browser applications), color variables can also be stored in json:

{"color": {
"parents": {
"baseStrong": "#000",
"baseWeak": "#fff",
"faintStrong": {
"default": "#8994A6",
"shiftDown": "#A5ADBB"
},
"faintWeak": {
"default": "#F6F8FB",
"shiftUp": "#EDEFF2"
},
"accentStrong": "#0070FF",
"accentWeak": "#EBF4FF",
"complementStrong": "#8889E2",
"complementWeak": "#EEECFD",
"criticStrong": "#F74545",
"criticWeak": "#FDEDED",
"warningStrong": "#F8AE4F",
"warningWeak": "#FCEBCF",
"successStrong": "#27AE60",
"successWeak": "#DEF8E9"
},
"context": {
"back": {
"faintWeak": "rgba(255, 255, 255, 0.15)"
},
"text": {
"faintWeak": "rgba(237, 241, 247, 0.5)"
},
"line": {
"faintStrong": "#EDEFF2"
},
"icon": {},
"fancy": {
"accentStrong": {
"default": "linear-gradient(132deg, #3F89EE, #5447FF)",
"shiftDown": "linear-gradient(132deg, #448FF3, #594CFF)"
}
}
}
}}

8.2. Color schemes in Figma

I believe that the best approach would be to divide the colors by contexts, so that the context names are always visible. Then you would add shifts to the very end of these sections. If the shifts are only used for the hover reaction, then you shouldn’t even add them to the palette, so they won’t distract you.

9. When it doesn’t make sense to use the system

9.1. Unusual design projects

Opium.Fill was created in such a way that the base colors must be black and white (or colors that are subjectively similar to black and white). If your project requires you to constantly write in blue on red or frequently use a ton of different colors in one interface, then Opium.Fill isn’t the best solution for the project.

9.2. Small projects

For a small project, it simply doesn’t make a lot of sense to use a system of pretty much any kind. It will be faster to throw all the colors into variables as you come across them (but it’s always better to use the list that the designer gave you). Sometimes a project can be so simple that you can just set colors without using any variables.

9.3. You are already using something else

If your team uses Material Design (among others) and everyone is happy with the way everything already is, then there is really no point in changing or adjusting something that already works well.

10. Objections

Below are a few frequently voiced problems regarding Opium.Fill. I will try to address some of them. If some of you encounter difficulties while working with the scheme, I would be glad if you shared them with me.

10.1. There are still a lot of variables

The careful reader has already calculated that we have available cells for 252 variables in Opium.Fill. How is that better than what you had before?

I can assure you that all of the colors will never be used simultaneously. Yes, there is plenty of space for new colors. But these (for the most part) will remain empty cells, like the undiscovered elements in Mendeleev’s periodic table. In real-life projects, we manage to “discover” up to about 30 elements per table.

If more than 50 variables happen to appear in your table, then something is probably going wrong and the system is, unfortunately, not helping the situation.

10.2. To fill charts and graphs, you have to greatly expand the table

If you have a lot of graphs or you need to color product categories using different colors (and there are, let’s say, 20 categories), then you will have to add new color families. The seven families that I described above will not be enough.

10.3. Why add substitutions if you could technically use shifts to do the same thing?

I base on the idea that contextual substitutions are a more “high-level” explanation of how designers use colors. Thus, substitutions are easier to understand and use.

Shifts are useful for rare colors, it’s better to use them in cases such as:

1. Event reactions (such as a hover or click)

2. When you need a broader range of various shades of gray

Conclusion

This concept originally took form at the beginning of 2018 and hasn’t changed much since then. In my beloved company, we implemented Opium.Fill both in design and development. Sometimes we develop products with designs made by other teams (from other companies), but this didn’t interfere with the use of the color scheme. We naturally ran into some challenges, but managed to solve them without too much difficulty.

If the topic of project’s color management is one that is particularly close to your heart, you may be interested in reading about other options such as: Material Design, Atlassian Design

Thanks for reading!

--

--