Color as a System: Naming, Schemes, and the Mechanics Between Them

Building a UI kit means making dozens of technical decisions before a single component is styled. Color handling is one of the trickiest of those decisions because it touches so many others: theme switching based on OS settings or user action, palette generation, contrast checking across different models, and configuration at various system levels. Digging into how established kits handle color reveals a set of recurring problems and a layered approach to solving them.

Defining the Working Vocabulary

Before discussing mechanics, it helps to pin down terms. The color space breaks down into two core dimensions: hue (the type of color, like red or blue) and the variants of that hue defined by brightness and saturation. A color palette is simply a set of those variants. All colors in a UI exist to fill some visual role for a component — a border, a text color, a background — which we can call visual properties.

A color scheme is the constraint layer that maps those roles to specific palette entries. It might restrict a component’s background to a particular hue, or adapt all colors to a dark environment or to users with vision impairments. It is useful to separate the term “color scheme” from the broader “theme,” which also encompasses typography and other non-color decisions.

Why Raw Color Names Fail

Consider a UI component like a call-to-action button: its background needs to change depending on the active scheme. Naming a variable redColor traps you, because in a different scheme that same variable would legitimately hold a blue value. Yet the components that signal an error state still need access to an actual red. The solution is an abstraction layer that assigns colors by role or visual property — an error state, a background — rather than by literal hue.

This role-based organization has precedent, even in CSS properties. Each entry in that layer maps to a palette entry by color name and variant, decoupling component definitions from specific color choices.

Remembering What Each Color Is For

Once you have this role layer, a secondary problem appears: remembering which named color is intended for what use. Comments and a shared glossary between design and engineering teams help establish a common language. Without that dictionary, teams end up translating domain terms inconsistently between code and conversation, which blunts communication.

Numbering the Variants

One straightforward approach is to append numbers to a hue’s name, where a higher number indicates a darker shade. The ease of adding new variants is a strength, but the numbers themselves carry little inherent meaning for a developer trying to pick the right one mid-task.

Unique Names vs. Scaled Values

Giving every variant a unique, arbitrary name is the least helpful option. It forces developers to memorize arbitrary strings or rely on a name generator — an extra dependency without adding useful information.

$gray-1: #eee;
$gray-2: #ccc;
$gray-3: #555;
css /* 100 is lightest, 900 is darkest */ $blue-100: #E3F2FD; $blue-500: #2196F3; $blue-900: #0D47A1;

A widely preferred system uses a scale from 10 to 100 in increments of ten, with lower numbers being lighter variants. A purple-10 is intuitively lighter than a purple-50, which keeps the system predictable as it grows. For accent shades, a prefix can be added, as many hue palettes include accent variants distinct from their main scale.

There is no perfect system here. If you must insert an intermediate brightness later, an enumerated scale forces you to shift every subsequent number. And any variant list comes with a maintenance burden: you must define the same set of shades for every hue so developers never discover that gray-20 exists but red-20 does not.

A Reference Point: Color Scales in Practice

Well-designed color systems handle these problems by focusing on conceptual clarity rather than technical implementation. The recommended approach is a numbered scale with a settled casing and ordering for the names, then semantic role tokens layered on top. This pattern is consistent across popular open-source kits — from ones that ship with enterprise React applications to the widely adopted Go component ecosystem with its design-system support for light and dark modes via semantic token overrides. Understanding how these kits map internal palette names to public API tokens is what makes their theming mechanisms usable in practice.

Two Implementations, One Question

To see how these theories play out in practice, let’s examine two popular React UI kits: Fluent UI React Northstar (@fluentui/[email protected]) and Material UI (@mui/[email protected]). Both take distinct approaches to organizing color systems.

Fluent UI React Northstar: Explicit Separation

In the “Teams” theme, Fluent UI React Northstar uses a two-dimensional color scheme model. “Brand” is a color scheme, as are “Light theme,” “HC theme,” and “Dark theme.” The color palette and the color scheme are explicitly separated into distinct keys within the theme’s siteVariables: the palette lives under colors, while the scheme is under colorScheme.

Color schemes
Color schemes. (Large preview)

Grouping by Visual Properties and States

The color scheme object’s keys combine visual properties with states, such as foregroundHover or backgroundColorActive. This tight coupling makes state-specific theming straightforward.

export const colorScheme: ColorSchemeMapping = {
  amethyst: createColorScheme({
    background: colors.amethyst[600],
    backgroundHover: colors.amethyst[700],
    backgroundHover1: colors.amethyst[500],
    backgroundActive: colors.amethyst[700],
  }),
};

A Functional Palette

The color palette itself is an object containing colors with functional names. Interestingly, some values are defined with transparency. The palette is organized into three distinct categories:

“Colors in Teams color palette have the following categorization.

Primitive colors

This part of the palette contains colors that, semantically, cannot have any tints. This group is represented by two colors, black and white — as there is nothing blacker than black and nothing whiter than white.

[...]

Natural colors

This part of the palette includes colors from those that are the most commonly used among popular frameworks (blue, green, gray, orange, pink, purple, teal, red, yellow). Each color includes at least ten gradients; this allows us to satisfy the most common needs.

This decision is experienced from Material UI and allows us to define more variants than by using semantical naming (lightest, lighter, etc.). However, there is no requirement for a client to define all the gradient values for each color — it is just enough to define those that are actually used in the app.

[...]

Contextual colors

This part of the palette may include brand color as well as danger, success, info colors, and so on.”

— “Colors”, Fluent UI documentation

The value for each color key can either be a string literal or an object containing keys for specific color variants.

export const colors: ColorPalette<TeamsTransparentColors> = {
  ...contextualAndNaturalColors,
  ...primitiveColors,
  ...transparentColors,
};
export const naturalColors: TeamsNaturalColors = {
  orange: {
    50: '#F9ECEA', // darkOrange[50]
    100: '#EFDBD3', // app orange14
    200: '#EDC2A7', // old message highlight border
    300: '#E97548', // orange[900]
    400: '#CC4A31', // app orange04 darkOrange[400]
    500: '#BD432C', // app orange03
    600: '#A33D2A', // app orange02
    700: '#833122', // app orange01 darkOrange[900]
    800: '#664134', // app orange14 dark
    900: '#51332C', // app orange16 dark
  },
}
export const primitiveColors: PrimitiveColors = {
  black: ‘#000’,
  white: ‘#fff’,
};

Material UI: A Unified Palette

Material UI offers only dark and light schemes by default. Everything is consolidated under the palette key in its theme configuration.

Screen of default scheme explorer
Screen of default scheme explorer. (Large preview)

Grouping by Function and Category

The keys within the color scheme are organized according to three principles:

  1. The functional purpose of the color, such as primary, text, error, warning, or divider. These keys hold nested variants like light, main, dark, and contrastText.
  2. Visual property name, such as background.
  3. Colors grouped in a category:
{
  common: {
    black: "#1D1D1D"
    white: "#fff"
  }
}

The theme.palette object also contains more than just colors — it holds the current color scheme mode and utilities like getContrastText.

{
  mode: 'dark',
}

An Imported Palette

Each named color (red, green, etc.) is an object whose keys correspond to color variants. The A prefix denotes an accent color.

const blue = {
  50: '#e3f2fd',
  100: '#bbdefb',
  200: '#90caf9',
  300: '#64b5f6',
  400: '#42a5f5',
  500: '#2196f3',
  600: '#1e88e5',
  700: '#1976d2',
  800: '#1565c0',
  900: '#0d47a1',
  A100: '#82b1ff',
  A200: '#448aff',
  A400: '#2979ff',
  A700: '#2962ff',
};

export default blue;

Which Is a Better Reference?

To select the best reference implementation, we weigh three factors: correspondence with agreed-upon terminology, implementation quality, and adherence to best practices.

Terminology Alignment

Fluent UI React Northstar has a clear advantage: it explicitly separates the palette from the scheme.

Material UI has a few terminological issues:

  • The “palette” key contains more than just colors.
  • The key name is confusing because importing the actual color palette requires pulling the “colors” object from the @mui/material package, not the palette.
  • It deviates from its own design guide: not all “on” colors are presented (and some are named differently), and the “surface” color also has a different name.

Naming and Grouping Practices

Aside from these core differences, both libraries use suffixes to denote brightness. Fluent UI’s palette includes functionally named colors alongside common color names, and its scheme groups colors by visual properties combined with states. Material UI uses a prefix for accent colors and groups its scheme by visual properties and function.

Given these factors, Fluent UI React Northstar is the stronger reference because it stays truer to the terminology explicitly separating palette from scheme. Had other design topics been in scope, the choice might differ.

Key Takeaways

  1. Before building your own system, study established references to avoid redundant work.
  2. Document the solved problems and terminology you discover during this review.
  3. Select solutions that satisfy your project’s specific needs and constraints.
  4. Choose the reference that best aligns with those chosen solutions.
  5. Base your implementation on that reference.

For deeper color theory, Rune Skjoldborg Madsen’s Programming Design Systems is highly recommended.