Author

Alex Baranov

Design engineer

Design systems Colors Semantic

Designing semantic colors for your system

This is a continuation of the article “Creating a Color Palette for Design Systems”. I recommend reading it first to understand how to build palettes, and then move on to this article.

What are semantic colors?

Semantic colors are colors assigned to convey specific meanings or states, such as “positive,” “negative,” “warning,” “primary,” etc. They link the visual elements of an interface to their function or state, making the design process intuitive.

For example, let’s say your primary color is red, and you’ve created a palette for this color with a complete set of tints and shades.

The red color palette in the design system.

Now you can use the created colors in your components and design elements. For instance, you’ll have red labels and buttons.

Using a color from the palette.

Everything seems logical and straightforward: you simply take the necessary tints and shades and apply them where needed. This works well until you need to change the primary color from red to blue during or after the design process. In such a case, you would have to manually update the color in every component.

Semantic colors eliminate this obstacle and allow you to create a system that simplifies the use of colors, especially when switching from one color to another.

Using semantic colors.

Semantic colors offer numerous advantages for designing, implementing, and using a design system. The key benefits include Consistency, Accessibility, Scalability, Flexibility, and Efficiency.

Consistency

Semantic colors ensure consistent use of colors across all components, enhancing both visual and functional coherence.

A single semantic color value is used for all labels.

For example, you can use a single semantic value for all labels, even though the tint parameters might vary depending on the intensity of the color or the desired effect. As a result, when applying a color, you don’t need to worry about the specific tint—you simply select the semantic value.

Accessibility

Semantic colors simplify the creation of accessible interfaces. You can predefine color pairs with sufficient contrast, ensuring reliable color usage.

The foreground of labels always maintains good contrast against the background.

A semantic accent color always passes the contrast test against a minimal background of the same color. This reduces the need to focus on the nuances of color application and allows you to concentrate more on business logic and design efficiency.

Scalability

Semantic colors provide a clear structure, making it easier to maintain and expand design systems.

The semantic color scale can be extended by creating additional values.

You can always extend semantic values while keeping the design consistent and logical, regardless of how the colors in your palette evolve.

Flexibility

Semantic colors allow for quick adaptation of the interface to new themes, modes (e.g., dark mode), or branding changes without the need for a complete overhaul of the color scheme.

Changing the primary color does not alter the semantic meaning.

This flexibility extends beyond the design phase. Developers can use tokens and variables to implement the interface and easily switch design themes or brand colors by simply updating the semantic values.

Efficiency

Defining colors based on their purpose rather than their appearance helps designers and developers work faster and more efficiently.

A color labeled “Primary.Accent” provides a clear understanding of its purpose for everyone involved. In contrast, a label like “Blue” offers no indication of how the color should be used.

Ultimately, semantic colors become a powerful design tool that simplifies both the creation process and, more importantly, the future expansion and modification of the design.

Different ways

There are several different approaches to creating semantic colors, each with its own advantages and disadvantages. We will explore some key approaches and analyze how each of them works.

Not using semantic colors

One approach is to not use semantic colors at all, and this can even work in practice. For example, this method is used in the CSS framework Tailwind.

Using colors without semantic meanings.

In this case, you control the colors of each component and can flexibly change them depending on the conditions and context. This approach provides incredible flexibility since you are not bound by an additional naming system that enforces specific rules.

However, there are significant downsides, such as a lack of design consistency and difficulties in making changes. It becomes quite challenging to update colors across all components if your brand color changes or if you want to make label backgrounds slightly lighter than they are now.

This also impacts the efficiency of the design process. When developing a new component, you constantly have to decide which tint or shade to use, ensure the colors are accessible, and verify that the overall design balance isn’t disrupted.

A system of functional colors

There are semantic color systems based on the functionality of each value. Notable examples of this approach include Radix UI Colors and the colors from the Vercel Geist system.

Radix UI Colors and Vercel Geist Colors.

These systems are exceptionally well thought out, with excellent logic that covers almost every color usage scenario for various components and design situations.

However, such systems have minor issues, mainly related to their flexibility. For instance, if you want to create a divider, it makes sense to use color 6 in Radix UI. But in your specific case, it might be too dark, and color 5 might be more suitable. This, however, breaks the system’s logic because color 5 is not intended for borders. Similar challenges can arise with other values, leading to occasional confusion.

Of course, you can always stick to the strict intended purposes, but in real-world design, there are often many situations where the rigidity of the system becomes an artificial limitation, preventing you from achieving your exact vision.

Additionally, such systems can be challenging to extend. For example, if you want placeholder text to be a soft gray, slightly lighter than value 11 in Radix UI, no such predefined color exists. You’d be forced to create a new value, which falls outside the system’s framework.

Design tokens

The token-based system for semantic colors is the most popular approach. It is widely used in many design systems and has proven to be the most effective and flexible method.

An example of a design token structure.

In this approach, you define the naming structure of tokens based on their properties, role, tone, and state. This creates a system that facilitates the creation, usage, and management of colors.

An example of using semantic colors.

I use this approach in my work, but with a few nuances that make it exceptionally flexible and universal. My naming system and token structure can work for designs of any complexity—from the simplest, using a minimal set of colors with just a few values, to large websites and applications with multiple states, administrative functions, and diverse design themes.

Naming

The foundation of my approach lies in a naming and notation system for semantic colors that covers all possible design tasks. If needed, the naming structure can be easily expanded or, conversely, reduced to a minimum. This flexibility is the cornerstone of a universal system.

There are various approaches to naming semantic colors. Often, these involve using comparative degrees to indicate tone intensity (e.g., strong…weak) or sequential naming like primary, secondary, tertiary, and so on.

Different notations of semantic scales.

While these naming systems can generally fulfill their purpose, they each have significant drawbacks that make them less suitable as a universal solution.

Limitations of existing systems

1. Flexibility and scalability

If you use a notation like strong…weak, you’re limited to six tone options. If you suddenly need more, it becomes an issue. Of course, you could add intermediate values like medium, but this risks breaking the consistency of the notation.

Additionally, such a system—like high…low—is not particularly intuitive in practical use. For instance, if you need to create a primary blue button, which color would be the accent: strong or strongest? Perhaps strongest is dark blue, and strong is light blue. But if strong is light blue, why is it called strong? As a universal solution, this approach often lacks clarity.

2. Sequential naming

A sequential system like primary, secondary, tertiary, etc., works well if you only have three tones—for example, primary for headers and main text, secondary for subtle text, and tertiary for placeholder text. However, if your design is more complex and requires multiple tones for backgrounds, borders, and other utility tasks, this system faces problems with scalability and logic.

Expansion becomes difficult because values beyond tertiary—like quaternary, quinary, senary—are rarely used, hard to remember, and impractical.

Additionally, the logic of this scale breaks down because it’s hard to align the meaning of a value with its name. For example, a primary background, used as the general background of an app, might be white, but a primary button background could be black. This makes it difficult to create a logical and consistent system where the meaning of a name intuitively reflects its tone and use case.

Background, foreground, border

To build the best possible scale and structure for semantic colors, it’s important to determine the specific use cases for these colors. Every interface element has just three properties that are expressed through color: background, foreground, and border. In many design systems, you might encounter different names for these properties:

Design systems often use different names for the same element properties.

Sometimes, this can be confusing—for example, both fill and background are used to describe backgrounds. The assumption is that fill refers to the solid colors of components, while background refers to general backgrounds for websites, apps, or groups of components.

I find this distinction unnecessary, as it makes color usage less intuitive. For instance, is the dark background of a card considered a fill or a background? It’s hard to tell.

Using a single term for all types of backgrounds simplifies both usage and understanding. That’s why I call any background, whether it’s for a component or an entire application, Background.

The surface of any element is considered its background.

Foreground refers to anything that appears on a background, even if the background is transparent. Therefore, any element color placed on a background—whether it’s text or an icon—is considered Foreground.

In some design systems, Foreground is divided into text and icon, as icons are sometimes slightly lighter than text to create a better balance when used alongside it. However, I believe this difference is too minor to justify creating a separate semantic color scale that would essentially duplicate the Text semantic scale.

For this reason, I always create a single scale for all types of Foreground. Later, I’ll explain how to handle cases where you want icons to be slightly lighter than text.

Any text or icon is considered foreground.

Borders are a bit simpler. They are typically just outlines, independent of other properties, so the scale is usually named either Border or Stroke, depending on your preference. I prefer Border, as it’s a more commonly used term in CSS and web applications.

The border of various design components.

Thus, my notation includes only three color properties:

  • Background
  • Foreground
  • Border

This immediately highlights the advantages of using semantic colors. In design tools, it becomes very convenient to adjust different properties since they have clear names. For example, to change text color, you simply update the Foreground color.

The button on the left has a semantic foreground and border color. The button on the right does not have a semantic color for each element property.

Without clear semantic names for properties, this would be more difficult, as the text color might, for example, match the border color. In such cases, you wouldn’t be able to quickly and easily change the text color in a design tool.

Background & Border

I have tested numerous approaches, evaluating their robustness, flexibility, and scalability, and arrived at a single option that meets the strictest requirements while remaining universal for any project.

I developed a notation system with highly intuitive names for backgrounds and borders, reflecting the intensity of the color tone and its meaning.

  • ghost
  • dimmed
  • minimal
  • subtle
  • medium
  • strong
  • intense
  • +
  • accent
  • accent-hover
  • +
  • static
  • rich
  • deep
  • abyss

The first group represents the gradation of tone intensity:

Tone intensity variations for a neutral color.

This group covers various levels of color intensity, allowing you to implement backgrounds and borders for any component or overall design. Ghost is the lightest tint, while Intense is the darkest.

You can create your own naming system for background and border scales that better aligns with the logic of your project or team. Personally, I find this set to be highly effective and intuitive.

Typically, there’s no need to use all seven steps of tone intensity. Of course, it depends on the design you’re creating, and often one to three steps are sufficient to achieve your goals.

The second group represents the Accent color, which is usually derived from the base color of the palette and is used for buttons, links, and other accent roles or states.

Accent and hover colors can be used for buttons.

The name “accent” ensures clarity, indicating that this value is the brightest, most prominent, and its purpose is obvious.

Additionally, you can extend the background and border scales by introducing new values, such as:

  • static – for solid colors that remain unchanged regardless of the design theme.
  • rich, deep, abyss – various degrees of shades for dark backgrounds.

For example, if your primary color is blue, you might use it for buttons and links. However, if your design includes large primary-colored backgrounds, it’s better to add a slightly darker and less contrasting blue, naming it rich or deep.

Advantages of this naming system

The flexibility of this naming system for backgrounds and borders is unmatched. You can always add any named value tailored to a specific component or use case without breaking the system.

For instance, you might need a special tone for a focus ring on primary buttons. No problem—you could name it primary/border-focus.

Examples of Background and Border variable scales for different color types in light and dark themes.

Sometimes, you may need a dark background that is black in the light theme and remains dark in the dark theme but slightly lighter than the default body background of the screen. This wouldn’t be a static color but a dynamic one, making it convenient to give it a specialized name.

An example of a variable for a dark background in both light and dark themes.

Foreground

For foreground, I use two notations. One is a scale of color intensity, where, similar to backgrounds and borders, there is an accent color, followed by gradations toward lighter tones.

Different foregrounds have their specific purposes.

The accent color is used for any headings, labels, and text, while subtle works well for secondary labels or text. Minimal is an excellent name that also indicates the minimally acceptable contrast for accessibility between the foreground and background. It is typically used for displaying disabled states or tertiary text.

Of course, you can expand the intensity scale by adding other values like dimmed or ghost. For instance, when designing a large heading, part of it can be significantly muted for contrast, using a dimmed tone.

Dimmed foreground can be used in large text elements.

For large text sizes, the contrast ratio will remain acceptable for accessibility despite its lightness.

The second Foreground group consists of specialized colors that can be used based on their purpose. For example:

  • heading — In content-focused websites and applications, readability is crucial. Headings can be slightly less contrasting than labels in components.
  • body — Similarly, body text in prose, articles, or other content can be slightly muted to reduce contrast with the background and improve readability.
  • static — Used for labels or icons that need to maintain the same color in any design theme.
  • icon — If it makes sense in your design, you can add a specific foreground for icons to make them slightly lighter than the adjacent text, ensuring better balance in contrast.

This way, the Foreground scale is also flexible and expandable, depending on the needs of your design.

Examples of Foreground variable scales for different color types in light and dark themes.

Start with the minimum

Always start with the minimum, especially when working on a project from scratch where many conditions and implementations are still unclear.

A simple set of semantic Primary colors for light and dark themes.

Create a set of only the essentials. In the beginning, this might be as simple as a single color like primary/bg-accent for buttons, plus white/fg-accent for labels on them. You can expand and add more as needed later.

Matching backgrounds and borders

I design the semantic scale for backgrounds and borders in a way that, for example, a minimal background pairs naturally with a minimal border, and a subtle background pairs with a subtle border. This makes it effortless to use these combinations when designing components.

Semantic names for Background and Border correspond to each other.

Of course, this isn’t a strict rule—you can pair a minimal background with a border of any color intensity if needed. However, having this default alignment simplifies the design process.

It’s also worth noting that it’s not always necessary to create corresponding borders for every background. For instance, if your design includes a minimal background but doesn’t use borders at all, creating border/minimal isn’t required.

Build your naming scale with room to grow

While you may start with only a minimal set of semantic colors, it’s best to design your naming system with future expansion in mind. This is why I’ve aimed to list and demonstrate the maximum number of possible values for all scenarios.

When you eventually need to expand your design system (and this will inevitably happen) or make changes to your design, it’s better to have this flexibility built into your naming system upfront to avoid any complications later on.

Universal structure

I divide all semantic colors into several groups. Four of them are essential for all my design systems, while others are added as needed.

Core and essential groups:

  • Surface — Colors for general backgrounds, elevation, layers, popup components, and focus state borders.
  • Black & White — Utility colors that either invert depending on the design theme or remain static.
  • Input & Link — Colors for input fields, input components, and links in text or other components.
  • Primary, Neutral & States — Accent colors or state-specific colors.

Surface

The Surface group includes specialized backgrounds and borders used to represent elevation, overlay layers, and focus states of elements. For example, it covers the general background of a website or application, backgrounds for popups and dropdowns, and focus borders.

In general, these colors help visually structure layers in a design, especially when considering dark themes. For instance, bg-raised in a light theme will match the general background, while in a dark theme, it will be slightly lighter, making the surface stand out.

Default semantic colors for surface:

  • bg-body — The general background of a website or application.
  • bg-base — Used for card backgrounds or other content, matching the general background.
  • bg-raised — Used in dark themes to highlight a surface, making it distinct from the general background.
  • bg-overlay — Used for the background of dropdowns, modals, and other popups. In dark themes, it’s usually lighter than the general background.
  • bg-overlay-hover — Used for hover states of popup elements, such as a dropdown menu item.
  • bg-overlay-active — Used for active states of popup elements, such as a selected item in a dropdown.
  • border-focus — Used for borders and focus rings when a button, input, or other component receives focus.

Example variables in Figma for the Surface group:

An example of a Surface variable scale in light and dark themes.

Black & White

This group includes four colors designed to solve the most basic and utilitarian design tasks, such as component or group backgrounds, borders, dividers, text, headings, and more.

This group consists of four neutral colors:

  • Base — Black tones with varying transparency that turn white in dark themes. For example, text will be black in light themes and white (or nearly white) in dark themes.
  • Inverted — White tones that, conversely, turn black in dark themes. For instance, if you have a dark background with a button featuring an inverted (white) background, in the dark theme, the background will become white, and the button black—inverted.
  • Black — A static black color that remains black in any theme.
  • White — A static white color that remains white in any theme.
An example of a Base variable scale in light and dark themes.
An example of an Inverted variable scale in light and dark themes.

Input & Link

This group includes semantic colors for implementing input components and links. The Input category encompasses colors for input fields, textareas, selects, checkboxes, and various custom components like toggles and input numbers.

Typical colors for input components in a design System:

  • fg-placeholder — Text color for placeholders inside inputs.
  • bg-input — Background color for input fields.
  • bg-input-focus — Background color for input fields in the focus state.
  • bg-input-on — Background color for input components in the “on” state, such as toggles.
  • bg-input-off — Background color for input components in the “off” state.
  • bg-input-disabled — Background color for input fields in the disabled state.
  • border-input — Border color for input fields.
  • border-input-focus — Border color when an input field is focused or the cursor is inside it.

I always create a dedicated group of colors for inputs in the design system, ensuring they are independent and easy to modify or extend. This approach simplifies the process of customizing input field borders and backgrounds if your project requires a unique design. For instance, you might want to make the border transparent and the background light gray.

Example variables in Figma for input colors:

An example of an Input variable scale in light and dark themes.

I create an extensive set of colors for links, including their hover and active states. These are the colors used for links in both components and text.

Links are often used in navigation components, making the active state particularly important in these scenarios.

An example of a Link variable scale in light and dark themes.

As you can see, I create link colors for all possible scenarios. This is very convenient because if a situation arises where a link needs to be placed on a colored accent background, I simply use link/white, and the link color is ready and will work perfectly.

Primary, Neutral & States

This group includes neutral, primary, and state-specific colors.

Neutral

Neutral colors differ from Base or Inverted as they are not transparent and are based on gray tones. They can have a hint of blue or other hues to create harmonious color shades. Neutral colors are used for similar general and utilitarian tasks, such as backgrounds, borders, text, and more.

An example of a Neutral variable scale in light and dark themes.

Primary & States

Primary and state-specific colors are used for accents and, as the name suggests, for various states: success, error, notifications, etc.

Examples of Primary, Negative, and Notice variable scales in light and dark themes.

This group can easily be expanded with any additional colors needed for your project. For example, if you need a Secondary color and decide on Mint, you can create its semantic colors based on your existing structure:

An example of a Secondary variable scale in light and dark themes.

Now, you have a new semantic color that can be used in your design. This flexibility allows infinite expansion as your project grows, enabling you to add colors based on their state or role, such as secondary, tertiary, or simply by naming the color, like Rose.

Custom

The Custom group is an optional set of semantic colors where I include specific backgrounds, borders, or foreground colors that are unique to a particular project. This is especially useful if the same design system is being used across multiple projects.

An example of a Custom variable scale in light and dark themes.

Creating this group helps maintain clarity about what is a default in the design system and what has been added as a project-specific customization.

Gradients

The Gradients group contains colors for gradients, specifying their starting, intermediate, and ending points.

An example of a Gradients variable scale in light and dark themes.

This is also an optional group of semantic colors. If your project doesn’t use gradients, this group isn’t necessary.

Charts

In the Charts group, I create colors specifically for data visualization, such as infographics or charts. This group is optional and only necessary if your design requires such elements.

An example of a Charts variable scale in light and dark themes.

I name the colors for data visualization using numbers to indicate tone when they are used globally across all types of charts. Alternatively, I create subgroups of colors for each specific chart type, such as dedicated colors for Pie or Line charts.

Conclusion

All the color groups and values listed above form a solid foundation for tackling tasks of almost any complexity in any project.

Of course, you can expand this system by adding your own color groups, such as for illustrations or specific interface sections. Likewise, any values within the groups can be extended or customized. It all depends on the project’s needs and requirements.

You can explore how my semantic color system works in a real design system like Superkube. This will help you see the concepts described above in action.

Additionally, if you haven’t read it yet, check out the article “Creating a Color Palette for Design Systems” to understand how to create color palettes for a design system.

Email me at [email protected] if you have any questions.


My books

Web Interface Handbook

A set of principles & best practices
for creating UI.

Designing Design Systems

How to make design systems
that work.

User Interface Typography

Creating balanced & expressive
UI typography.


Subscribe for news about upcoming
books and my projects

Stay tuned and be the first to know about the release of my new
books and future projects.