CSS Hanno Zhuan Color System

I make a simple color system so that every developer can be a color designer. Your only requirement is CSS knowledge.

By Hanno Zhuan

You might finish reading in 12 minutes

I have a huge announcement, folks. I am not good at managing 100 colors. I can not juggle either. This is why I can not use too many colors at the same time, like the Steam web developers. Thus, I make a simple color system for this website, and I hope that this system can be adopted into many projects. I call it the CSS Hanno Zhuan Color System.

I am not following any design trends. But I often use X and YouTube. They use mostly black and white colors. The YouTube subscribe button, for example, does not use red color anymore. It uses a white background and black text. The same goes for the X post button. When it was Twitter, the tweet button had a blue background with white text. Now X post button has a white background with black text. That makes me wonder, "Why do those big websites use a simple color theme?" The reason is simplicity. Therefore, I decided to adopt their color approach.

I am developing a simple color system that works for almost all projects. Minimalist developers may like it. But I certainly love this system. I really do.

Warming up with two colors only

The simplest color system is just one black color and one white color. We can have a dark theme and a light theme easily. Just have the following custom properties, and we get a nice website:

:root {
  --color-dark: #111;
  --color-light: #eee;
  --color-body-background: var(--color-light);
  --color-body-text: var(--color-dark);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-body-background: var(--color-dark);
    --color-body-text: var(--color-light);
 }
}

body {
  background-color: var(--body-background-color);
  color: var(--body-text-color);
}

If you want to make a card component with a border, you can use currentColor keyword for the border color.

.card {
  border: 1px solid currentColor;
}

But you can simplify the dark theme by using the CSS filter property:

/* The rest is still the same */

@media (prefers-color-scheme: dark) {
  :root {
    filter: invert(1);
 }
}

/* Attribution: https://heydonworks.com/css/styles.min.css */

That works well since we only use two colors, and they are neutral colors—black and white.

Now this little warm-up sets your mentality to understand the CSS Hanno Zhuan Color System.

Having the necessary colors

The main colors that you must have are black and white. You must have those colors.

You can use the following colors to get started:

:root {
  --color-dark: #181617;
  --color-light: #ffffff;
}

Those two colors will the body background color and the body text color. This means your website will have black and white colors as the majority for almost all elements or components.

Personalizing your website

We will have three accent colors.

I chose three colors because each of them complements each other. For example, we have two colors: blue and yellow. The blue one works for the light theme. The yellow one works for the dark theme. But do not forget that we can have a highlighted section with an accent color for the background. In this case, we need to have an accent color for our accent color. As an example, if the background color is blue and the text color is white, what is the accent color for the blue background? The black color will not work well, so we need a third color. In this case, we can use the yellow color. In dark mode, the highlighted section will have the yellow background with black text. But what is the accent color that works with yellow? I will not use blue because it does not match. Instead, I will use purple as the new accent color for the yellow background.

Simply, we will have this pseudocode for our color system:

Color accent: yellow, purple, and blue

DARK THEME = black background color AND white text color
LIGHT THEME = white background color AND black text color

IF DARK THEME
  THEN: yellow accent color
IF LIGHT THEME
  THEN: blue accent color
IF yellow background color
 THEN:
 - black text color
 - purple accent color
IF purple background color
 THEN:
 - white text color
 - yellow accent color
IF blue background color
 THEN:
 - white text color
 - yellow accent color

Adding depth to our colors

The light theme will use a pure white—#fff. This means the other color should be darker. Meanwhile, the dark theme will use almost pure black, so we need a lighter color. With these two new colors, we can highlight a section so that it does not blend with the body background.

Those two colors serve a different purpose than the accent colors, in terms of highlighting sections. Accent colors guide users to notice specific sections. You can use one of them as the background for ads, announcements, or anything you want your users to notice. In contrast, the two colors—darker white and lighter black—are for decoration only. For example, you can use one of them to make the footer stand out from the body background. You do not have any other intentions besides that.

We can have the following colors:

:root {
  --color-dark-glare: #221f20;
  --color-light-shade: #f1f1f1;
}

You decide for yourself how much contrast you want from the body background for each theme. But these colors must have good contrast ratio with the current body text color, because I do not want to add new colors, and I want to keep the CSS Hanno Zhuan Color System simple.

Tracking all colors

We do not track all HTML elements because most of them will inherit the color of the body text. We only track colors that do not inherit the body text color or colors that we want to change.

  1. The body background color
  2. The body text color
  3. The link's underline
  4. The ::selection background color
  5. The ::selection text color
  6. The mark background color
  7. The mark text color
  8. The code background color
  9. The code text color
  10. The table border color
  11. The focus outline color
  12. The current page link background color
  13. The current page link color
  14. The input text box background color
  15. The input text box border color
  16. The input text box text color
  17. The pre background color
  18. The pre text color
  19. The pre border color (optional)
  20. The pre scrollbar background color
  21. The pre scrollbar color
  22. The ins background color
  23. The ins text color

Then, these are the colors that use the accent colors:

  1. The link's underline
  2. The mark background color
  3. The code text color

If you have more components, you can use accent colors to personalize them. For example, a button component can have an accent color for the background color. Use your imagination to improvise.

Having independent colors and dependent colors

I divide all colors that we track into two groups:

  • independent colors; and
  • dependent colors.

The independent colors do not depend on the body background color and text color. They are independent and have their own rules. For example, pre elements do not need to care about the body background color. They have their own color setup. This means, regardless of the site's theme and the parent element's theme, the pre color theme will not change.

The dependent colors depend on the body background color and text color. For example, the table border color depends on the background of the parent element, which is the body element in this case. Thus, if the body has a dark background, the table color will have light color. The table color will match the body text color.

Luckily, the only independent colors are all sets of pre colors.

:root {
  --color-pre-background: var(--color-dark-glare);
  --color-pre-text: var(--color-light);
  --color-pre-border-block: var(--color-accent);
  --color-pre-scrollbar-background: var(--color-pre-text);
  --color-pre-scrollbar-scroll: var(--color-dark);
}

The rest of the colors will depend on the parent element.

Having local colors and global colors

I know that the <table> element will not always have the body as the parent element. What if the table is inside an element with an accent background color? This is why we need color scopes: local colors and global colors.

Local colors are CSS custom properties for specific context. Meanwhile, global colors are always defined with an assumption that the elements depend on the body background color. If the local colors are not set, the global colors will be used. For instance, the ::selection relies on the parent element's background color. To make the ::selection colors adapt, we use CSS custom properties with the following setup:

:root {
  --global-color-selection-background: var(--color-body-text);
  --global-color-selection-text: var(--color-body-background);
}

::selection {
  background-color: var(
    --color-selection-background,
    var(--global-color-selection-background)
 );
  color: var(--color-selection-text, var(--global-color-selection-text));
}

Here is how I interpret the above code:

  1. I set up the global colors for the ::selection.
  2. If the --color-selection-background and --color-selection-text are empty, they will fallback to use the global custom properties. This means those HTML elements depend on the body background color.
  3. If the --color-selection-background and --color-selection-text are defined, I assume the elements are under a different set of color themes.

For example, the pre elements will not use the global properties because they have their own color for the ::selection. Thus, we do the following:

pre {
  --color-selection-background: var(--color-pre-text);
  --color-selection-text: var(--color-pre-background);
}

This way we do not need to keep writing pre ::selection. We do not even need to write any ::selection anymore. We just use the custom properties to target the ::selection pseudo-element.

I list some elements that have global color properties because they can exist under different parent elements:

  • The outline for :focus-visible
  • All sets of table colors
  • The input elements
  • Other components that you can imagine, such as buttons and cards

Understanding the CSS Hanno Zhuan Color System

I wrote the following pseudocode for this, and it turned out really well:

Context: Light theme

body background = LIGHT
body text = DARK

Link underline = ACCENT
[aria-current] background = body background highlight
[aria-current] text = body text

GLOBAL selection background = body text
GLOBAL selection text = body background

mark background color = ACCENT
mark text color = body background

code color = ACCENT
code background = body background highlight

GLOBAL table border = body text

GLOBAL focus outline = body text

GLOBAL fieldset border = body text

GLOBAL input background = body text
GLOBAL input text = body background
GLOBAL input border = body text
GLOBAL input disabled background = body background highlight

pre background = DARK GLARE
pre text = LIGHT
pre scrollbar background = LIGHT
pre scrollbar = DARK
pre ::selection background = pre text
pre ::selection text = pre background

The elements' color that rely on the ACCENT color do not need to be localized because the accent color will use a generic custom property (--color-accent). You will see the code snippet. But simply, if we want to change the accent color, we can just do the following:

IF yellow background color
 THEN:
 - black text color
 - ACCENT = purple

The <mark> and <code> elements usually depend on the body background color. If you use the <mark> element, this means you only want to highlight specific text. You use an accent color to highlight large content. The <code> element may have a chance to appear on an accent background color, but I will not worry about that until it happens.

Here is a complete code snippet for light and dark mode. Use it to test if it works on your website:

/**
 * Copyright 2025 Hanno Zhuan
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

/**
 * Hanno Zhuan Color System
 * Copyright 2025, Hanno Zhuan: https://hannozhuan.nelify.app
 * Released under Apache License 2.0 - Apache-2.0 https://www.apache.org/licenses/LICENSE-2.0.txt
 * Please leave the comments intact as an attribution
 */
:root {
  --color-primary-accent: #f2df0d;
  --color-secondary-accent: #0f3888;
  --color-tertiary-accent: #1e0820;
  --color-dark: #181617;
  --color-dark-glare: #221f20;
  --color-light: #ffffff;
  --color-light-shade: #f1f1f1;

  --color-accent: var(--color-secondary-accent);
  --color-body-background: var(--color-light);
  --color-body-background-highlight: var(--color-light-shade);
  --color-body-text: var(--color-dark);
  --global-color-selection-background: var(--color-body-text);
  --global-color-selection-text: var(--color-body-background);
  --global-color-focus-ring: var(--color-body-text);
  --global-color-table-border: var(--color-body-text);
  --global-color-table-scrollbar-background: var(--color-dark-glare);
  --global-color-table-scrollbar-scroll: var(--color-body-text);
  --global-color-input-border: var(--color-body-text);
  --global-color-input-background: var(--color-body-text);
  --global-color-input-text: var(--color-body-background);
  --global-color-input-disabled-background: var(
    --color-body-background-highlight
 );
  --color-pre-background: var(--color-dark-glare);
  --color-pre-text: var(--color-light);
  --color-pre-border-block: var(--color-accent);
  --color-pre-scrollbar-background: var(--color-pre-text);
  --color-pre-scrollbar-scroll: var(--color-dark);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-accent: var(--color-primary-accent);
    --color-body-background: var(--color-dark);
    --color-body-background-highlight: var(--color-dark-glare);
    --color-body-text: var(--color-light);
    --global-color-table-scrollbar-background: var(--color-light-shade);
 }
}

body {
  background-color: var(--color-body-background);
  color: var(--color-body-text);
}

::selection {
  background-color: var(
    --color-selection-background,
    var(--global-color-selection-background)
 );
  color: var(--color-selection-text, var(--global-color-selection-text));
}

:focus {
  outline-color: var(--color-focus-ring, var(--global-color-focus-ring));
}

@supports selector(: focus-visible) {
  :focus {
    outline: none;
 }

  :focus-visible {
    outline-color: var(--color-focus-ring, var(--global-color-focus-ring));
 }
}

a {
  color: currentColor;
  text-decoration-color: var(--color-accent);
}

[aria-current] {
  background-color: var(--color-body-background-highlight);
  color: var(--color-body-text);
}

mark {
  background-color: var(--color-accent);
  color: var(--color-body-background);
}

code {
  color: var(--color-accent);
  background-color: var(--color-body-background-highlight);
}

pre {
  --color-selection-background: var(--color-pre-text);
  --color-selection-text: var(--color-pre-background);
  background-color: var(--color-pre-background);
  color: var(--color-pre-text);
  scrollbar-color: var(--color-pre-scrollbar-background)
    var(--color-pre-scrollbar-scroll);
 border-block: 2px dashed var(--color-pre-border-block);
}

td,
th {
  border-color: var(--color-table-border, var(--global-color-table-border));
}

thead th {
  background-color: var(--color-body-background-highlight);
}

th:not(:only-of-type) {
  border-block-end: 1px solid
    var(--color-table-border, var(--global-color-table-border));
}

th:only-of-type {
  border-inline-end: 1px solid
    var(--color-table-border, var(--global-color-table-border));
}

:is(th, td) ~ :is(th, td) {
  border-inline-start: 1px solid
    var(--color-table-border, var(--global-color-table-border));
}

tr + tr :is(th, td) {
  border-block-start: 1px solid
    var(--color-table-border, var(--global-color-table-border));
}

.table-container {
  scrollbar-color: var(
      --color-table-scrollbar-background,
      var(--global-table-scrollbar-background)
 )
    var(--color-table-scrollbar-scroll, var(--global-table-scrollbar-scroll));
}

ins {
  background-color: var(--color-body-background-highlight);
  color: var(--color-body-text);
}

input:disabled {
  background-color: var(
    --color-input-disabled-background,
    var(--global-color-input-disabled-background)
 );
}

:is(
  input:not([type="checkbox"], [type="radio"], [type="color"]),
  select,
  textarea
) {
  --color-selection-background: var(
    --color-input-text,
    var(--global-color-input-text)
 );
  --color-selection-text: var(
    --color-input-background,
    var(--global-color-input-background)
 );
  border: 2px solid var(--color-input-border, var(--global-color-input-border));
  background: var(
    --color-input-background,
    var(--global-color-input-background)
 );
  color: var(--color-input-text, var(--global-color-input-text));
}

Using guidelines

Some guidelines are needed to keep the system simple and easy to adjust:

  • The accent color must only have one variant. To highlight a section within a parent element with an accent background color, use whitespace, border, or another method.
  • Other guidelines will be added if necessary.

Adopting this simple system

Feel free to use this system. If you have requests or questions, you can contact me.

Happy coloring!