CSS Color Architecture

Elad Shechter
Appwrite
Published in
11 min readJan 30, 2023

One of the hardest things in programming languages is organizing code in a way that will be easy to understand and maintain.

In this post, I want to explain how I work and organize the colors in our Pink Design system project.

Like everything in life, we have many good ways and even more wrong ways to do the same thing. Before I show you how I organize our CSS variables, let’s discuss the wrong ways to organize colors in CSS.

Note: some parts of our color architecture use Sass preprocess.

The Wrong Ways to Define CSS Color Variables

When reviewing other CSS architecture, I try to think of inefficiencies that make it difficult to maintain.

Using Globals variables for Everything Without Semantic Meaning

I took this small example using the inspect feature from the :root element on the LinkedIn website.

As you can see in the example below, there are more than 900 CSS variables on one :root selector!

Finding something in such an extensive list of variables is almost impossible.

Coupling Colors with Semantic Names

The second option I frequently see is global variable color with semantic names, such as --header-background-color.

The problem with defining logical global naming, first and foremost, is that you have too many of them.

The second problem with creating dark-mode themes is that web designers do not work with the logic that specific colors need to change to other colors in dark-mode. This can cause the creation of too many types of variables that are hard to understand or maintain.

Example from old Appwrite console 1.0:

Furthermore, the overrides of global colors make the code’s debugging unclear, with all the crossline of the overrides in the inspect element of Chrome.

After understanding these issues, I thought about how to better structure our CSS color variables.

How We Decided to Manage Our CSS Color Variables

My main idea was to create a CSS variable for every color group family on the hue spectrum that would connect every group of colors (e.g., blue, green, orange, and red). In this way, I can change the primary hue of each color group and easily replace all shades of this color family.

The only issue I had with this approach was that the color had been defined with hex code colors, by the design team, which are a type of RGB colors and are more difficult to use when creating different shades of the same hue.

In this case, I chose to convert the HEX/RGB colors into HSL colors and try to find the typical hue of every group of colors.

Converting from HEX/RGB to HSL

If we take the information (blue) colors and convert them into HSL colors, we can see that all the hues (first value) are different; they range between 188 and 192.

The main idea was to keep the first value as another CSS variable.

:root {
--color-info-hue: /* ? */;

--color-info-10: 189 87% 97%; /* #F1FCFE */
--color-info-50: 192 90% 89%; /* #C8F2FC */
--color-info-100: 189 100% 38%; /* #00A7C3 */
--color-info-120: 190 100% 26%; /* #007187 */
--color-info-200: 188 87% 12%; /* #04333A */
}

To solve this issue, I decided to use the CSS calc() function, subtracting or adding the difference to the base hue value.

I decided to take the base hue from the primary color of every family group; in our case, the primary color was the “100” color, and the hue of the info (blue) family group was 189.

The result looked like this:

:root {
--color-info-hue: 189;

--color-info-10: var(--color-info-hue) 87% 97%; /* #F1FCFE */
--color-info-50: calc(var(--color-info-hue) + 3) 90% 89%; /* #C8F2FC */
--color-info-100: var(--color-info-hue) 100% 38%; /* #00A7C3 */
--color-info-120: calc(var(--color-info-hue) + 1) 100% 26%; /* #007187 */
--color-info-200: calc(var(--color-info-hue) - 1) 87% 12%; /* #04333A */
}

In this way, I can play with the hue and change all colors created from it.

How to Use These Variables?

When using the color variables, every call must be wrapped with the hsl() function. For example:

background-color: hsl( var(--color-info-100) );

The hsl() function wasn’t added to the variable itself because I want an easy way to control the opacity of the colors if needed.

Example:

background-color: hsl( var(--color-info-100) / 0.5 ); /* with 50% opacity */

Private Local Variable Logic

Because we have different color modes (light/dark modes), in most cases, every partial’s color will change to another color in the second color mode.

In my method, all the colors are global, both light and dark mode colors. To manage the light and dark mode’s color, I added local color variables, which will reference global color variables according to the mode.

To not have a “mess” of too many global variables, I use the concept of private variables for each partial.

To indicate a variable is private, I start it with p-. For example:

.partial {
--p-variable-name: value;
}

In our button partial, for example, I have the main private variable for the text color, background-color, and border-color.

.button {
/* Light-mode Theme */
--p-text-color: value;
--p-button-color: value;
--p-border-color: value;
}

The usage of those variables looks like this:

.button {
color: hsl( var(--p-text-color) );
background-color: hsl( var(--p-button-color) );
border-color: hsl( var(--p-border-color) );
}

Variables in a complex partial can have a lot of states. For example, a button can have a default, :hover, :focus, :active, or :disabled state. Those essential inner variables use other inner variables to present any state.

The code of my button variables looks like this:

.button {
/* Light Theme */
--p-text-color: var(--p-text-color-default);
--p-button-color: var(--p-button-color-default);
--p-border-color: var(--p-border-color-default);

--p-text-color-default: var(--color-neutral-5);
--p-button-color-default: var(--color-primary-200);
--p-border-color-default: var(--color-primary-300);

--p-text-color-hover: var(--p-text-color-default);
--p-button-color-hover: var(--color-primary-100);
--p-border-color-hover: var(--p-border-color-default);

--p-text-color-focus: var(--p-text-color-default);
--p-button-color-focus: var(--color-primary-200);
--p-border-color-focus: var(--color-primary-200);

--p-text-color-active: var(--p-text-color-default);
--p-button-color-active: var(--color-primary-300);
--p-border-color-active: var(--color-primary-300);

--p-text-color-disabled: var(--color-neutral-50);
--p-button-color-disabled: var(--color-neutral-10);
--p-border-color-disabled: var(--color-neutral-10);
}

Defining States of Button

What is nice now is that I only need to update the variable’s value whenever I want to change the buttons.

These variables can, then, be applied based on the state of the partial.

Basic State Definitions (written in Sass):

/* global Sass Variable */
$disabled: ":disabled, .is-disabled";

.button {
&:is(:hover) {
&:where(:not(#{$disabled})) {
--p-text-color: var(--p-text-color-hover);
--p-button-color: var(--p-button-color-hover);
--p-border-color: var(--p-border-color-hover);
}
}
&:is(:focus-visible) {
&:where(:not(#{$disabled})) {
--p-text-color: var(--p-text-color-focus);
--p-button-color: var(--p-button-color-focus);
--p-border-color: var(--p-border-color-focus);
}
}
&:is(:active) {
&:where(:not(#{$disabled})) {
--p-text-color: var(--p-text-color-active);
--p-button-color: var(--p-button-color-active);
--p-border-color: var(--p-border-color-active);
}
}
&:where(#{$disabled}) {
--p-text-color: var(--p-text-color-disabled);
--p-button-color: var(--p-button-color-disabled);
--p-border-color: var(--p-border-color-disabled);
}
}

I use the Sass variable $disabled so that I can use the style of the disabled button on other elements, such as link elements.

Sass Code:

/* global Sass Variable */
$disabled: ":disabled, .is-disabled";

.button {
&:where(#{$disabled}) {
--p-text-color: var(--p-text-color-disabled);
--p-button-color: var(--p-button-color-disabled);
--p-border-color: var(--p-border-color-disabled);
}
}

Compiled CSS:

.button:where(:disabled, .is-disabled) {
--p-text-color: var(--p-text-color-disabled);
--p-button-color: var(--p-button-color-disabled);
--p-border-color: var(--p-border-color-disabled);
}

Will target:

<button class="button" disabled> </button>

<a class="button is-disabled"> </a>

Dark Mode Treatment

After taking care of all the button light-mode states, we now want to take care of our dark-mode states.

Before doing so, I define another global Sass variable representing the dark-mode CSS class state. This state class name will be used in most of our partials to create unique colors for dark mode.

$theme-dark: ".theme-dark";

The .theme-dark class is better defined directly on the <html> element, of course, only when you use the dark-mode state.

If defining it on the <html> element is an issue, it can be defined on the <body> element instead.

<body class="theme-dark"> </body>

This is done to achieve easy global control of all the HTML elements.

Dark-Mode Treatment Inside the Partial

To create the definition of dark mode in the button partial, I add this code segment at the bottom of the partial:

.button {
/* regular styles and light-mode definitions */

#{$theme-dark} & {
/* definitions for dark-mode */
}
}

This Sass code will compile to this selector:

.button { /* regular styles and light-mode definitions */ }

.theme-dark .button { /* definitions for dark-mode */ }

Because all the states of the buttons are already declared, the only thing left to do is to define the states’ private color variables in the dark-mode “section”.

If some colors remain the same, they do not need to be overridden in dark mode.

.button {
#{$theme-dark} & {
/* changed colors */
--p-border-color-default: var(--color-primary-200);

--p-button-color-hover: var(--color-primary-100);
--p-border-color-hover: var(--color-primary-100);

--p-border-color-focus: var(--color-primary-300);

--p-border-color-active: var(--color-primary-300);

--p-text-color-disabled: var(--color-neutral-100);
--p-button-color-disabled: var(--color-neutral-150);
--p-border-color-disabled: var(--color-neutral-150);
}
}

What is nice about this method is that we do not need to repeat any CSS selectors or any properties definitions.

More Types of Buttons

In our project, we needed to have different types of buttons.

Because we have already created a solid structure, we only need to define those variables according to the new state of the button.

Define New State

To define a new state, we add our new state class (.is-secondary):

<button class="button is-secondary"></button>

Now, to update the colors for the new type of button, we just override the private colors:

.button {
&.is-secondary {
/* Light Mode */
--p-text-color-default: var(--color-neutral-100);
--p-button-color-default: var(--color-neutral-5);
--p-border-color-default: var(--color-neutral-30);

--p-text-color-hover: var(--p-text-color-default);
--p-button-color-hover: var(--color-neutral-10);
--p-border-color-hover: var(--p-border-color-default);

--p-text-color-focus: var(--p-text-color-default);
--p-button-color-focus: var(--p-button-color-default);
--p-border-color-focus: var(--transparent);

--p-text-color-active: var(--color-neutral-300);
--p-button-color-active: var(--color-neutral-30);
--p-border-color-active: var(--color-neutral-30);

--p-text-color-disabled: var(--color-neutral-50);
--p-button-color-disabled: var(--p-button-color-default);
--p-border-color-disabled: var(--color-neutral-30);

/** Dark Mode **/
#{$theme-dark} & {
--p-text-color-default: var(--color-neutral-5);
--p-button-color-default: var(--color-neutral-300);
--p-border-color-default: var(--color-neutral-150);

--p-text-color-hover: var(--p-text-color-default);
--p-button-color-hover: var(--transparent);
--p-border-color-hover: var(--color-neutral-120);

--p-text-color-focus: var(--p-text-color-default);
--p-button-color-focus: var(--p-button-color-default);
--p-border-color-focus: var(--transparent);

--p-text-color-active: var(--p-text-color-default);
--p-button-color-active: var(--p-button-color-default);
--p-border-color-active: var(--color-neutral-100);

--p-text-color-disabled: var(--color-neutral-100);
--p-button-color-disabled: var(--p-button-color-default);
--p-border-color-disabled: var(--color-neutral-150);
}
}
}

As you can see, I’m only defining variables here, without any properties and or any state selector pseudo-class like :hover, :focus and so on.

CodePen Full Demo:

Open CodePen Demo in separate tab

Global Colors State

In most cases, we do not want to define global color variables that are updated to other colors in dark mode.

However, while this is correct for most cases, in some specific cases we will want to define a state color that looks like one specific color in light mode and another in dark mode.

Global Logic Colors

For that, I created another solution, which I am calling “global logic colors.”

For these, I created global CSS variables that are defined in a separate :root selector; of course, they reference other global color variables.

For dark mode, these variables are changed to another global color variable. Example:

:root {
/* Global Logic Colors - Light Mode */
--color-text-info: var(--color-info-100);
--color-text-danger: var(--color-danger-100);
--color-text-warning: var(--color-warning-100);
--color-text-success: var(--color-success-100);

--color-border: var(--color-neutral-10);
--scroll-color: var(--color-neutral-50);

#{$theme-dark} {
/* Global Logic Colors - Dark Mode */
--color-text-info: var(--color-info-120);
--color-text-danger: var(--color-danger-120);
--color-text-warning: var(--color-warning-120);
--color-text-success: var(--color-success-120);

--color-border: var(--color-neutral-200);
--scroll-color: var(--color-neutral-150);
}
}

These CSS variables are used in two ways:

  1. Direct usage inside a partial
.icon-checked { color: hsl( var(--color-text-success) ); }

2. As a global utility class

/* Global Utilities colors classes */
.u-color-text-disabled { color: hsl( var(--color-text-disabled) ); }
.u-color-text-offline { color: hsl( var(--color-text-offline) ); }
.u-color-text-info { color: hsl( var(--color-text-info). ); }
.u-color-text-danger { color: hsl( var(--color-text-danger). ); }
.u-color-text-warning { color: hsl( var(--color-text-warning). ); }
.u-color-text-success { color: hsl( var(--color-text-success) ); }

The global utility classes can be used directly on an element and will provide different colors according to the light mode or dark mode theme.

In both ways, the colors are updated according to the state of the color mode scheme.

To Summarize

In this post, we discussed common ways to define CSS color variables and their problems. After that, we explored how to reorganize CSS variables using private variables to create a CSS architecture with well-organized colors.

Final Words

Pink Design is finally out today. Pink Design was built with new inventive CSS architectures and prioritizes accessibility and developer experience. As a company developing an open-source product, this is a big step for Appwrite’s community of contributors. As always, we will keep improving Pink Design, allowing it to grow alongside Appwrite.

Thank you for being a part of our journey! If you haven’t already, browse the 💻 Pink Design GitHub Repository or check out our 🚀 Getting Started Guide to use Pink Design in your projects or when contributing to Appwrite.

Until next time!

--

--