Thinking Deeply About Theming And Color Naming

Learn a systematic approach to naming CSS colors that prioritizes maintainability, themeability, and long-term project health.

Introduction: Why Naming Matters

Naming colors in CSS is one of those tasks that seems simple until you face a rebranding. You created a beautiful color palette with descriptive names like --merlot and --crimson, only to discover months later that your "merlot" is now navy blue and your "crimson" has become cobalt. The variable names no longer match their values, and you're stuck with a codebase full of misleading identifiers that create confusion and technical debt.

The challenge lies in choosing variable names that aren't tied to specific color values. This concept has existed in computer science since the beginning--it's called information hiding, a core principle of encapsulation. When you name variables based on their semantic purpose rather than their current visual appearance, you create systems that can evolve without requiring widespread code changes.

This article explores a practical approach to color naming that prioritizes themeability and long-term maintainability, along with modern CSS features that make implementing robust theming systems more straightforward than ever. For teams building comprehensive design systems, establishing consistent color naming patterns early prevents technical debt from accumulating over time.

The Problem with Descriptive Color Names

Many developers start by naming colors descriptively--based on the hue they currently represent. A quick grep through countless codebases reveals patterns like --ocean-blue, --forest-green, --sunset-orange, or --deep-purple. While these names are intuitive and easy to remember during initial development, they create significant problems as projects evolve.

Why Descriptive Names Fail

When rebranding occurs, these descriptive names become misleading. Your --ocean-blue might now be a deep teal, but the variable name still says "blue." New developers joining the project will be confused when using a "blue" variable that renders as green. The cognitive mismatch between name and appearance slows down development and increases the risk of errors.

Even more problematic are situations where color values need to change within the same "category." If you have --primary-blue and --secondary-blue, and the design calls for both to shift toward purple, you now have two variables whose names don't reflect their actual appearance. Over time, these mismatches accumulate, and your codebase becomes harder to maintain.

The Solution

The solution isn't to abandon meaningful names entirely--it's to shift from value-based naming to purpose-based naming.

The AIR Naming Convention

The AIR convention provides a systematic approach to naming color variables that focuses on affiliation, ID, and relationship rather than specific visual properties. This three-part structure creates a flexible foundation that can accommodate theme changes without requiring variable name modifications.

The Three Pillars

ComponentPurposeExamples
AffiliationGroup colors by purposebrand, site, accent, success, error
IDDistinguish colors within groups1, 2, 3...
RelationshipDefine color rolesc (contrast), variations

The resulting variable names follow this pattern: --color-affiliation-id-relationship

Affiliation: Grouping Colors by Purpose

The first component of an AIR variable name identifies the color's affiliation--essentially, which part of your design system or brand it represents. Common affiliations include:

  • brand: Primary brand colors that define your visual identity
  • site: General site-wide colors for backgrounds, text, and UI elements
  • accent: Secondary colors for highlighting and emphasis
  • success: Colors indicating positive outcomes or confirmations
  • error: Colors signaling problems, failures, or warnings
  • warning: Colors for cautionary states that require attention
  • info: Colors for informational states or neutral notifications

These affiliations establish logical groupings that make your color system organized and discoverable. A developer looking at --color-brand-1 understands immediately that this color relates to the brand identity, even without knowing its exact value.

ID: Distinguishing Colors Within Groups

After establishing affiliation, you need to distinguish between colors within the same group. Rather than using ordinal terms like "primary," "secondary," and "tertiary" (which become awkward after a few colors and aren't used in everyday language), the AIR convention uses simple numbers.

This approach has several advantages. Numbers are universally understood, easy to sequence, and don't carry the linguistic baggage that words like "tertiary" or "quaternary" might for non-native speakers. The pattern scales naturally--just add the next number in sequence.

--color-brand-1
--color-brand-2
--color-brand-3

The numeric ID also makes it easier to create systematic relationships between colors. When you know that --color-brand-1 is your primary brand color, you can infer that variations like --color-brand-1-c relate to it in specific ways.

Relationship: Defining Color Roles

The final component identifies the relationship or role that a color plays relative to its affiliation and ID. The most common relationship is contrast (abbreviated as "c" for brevity), which designates a color suitable for placing against the base color.

Consider a button component. The button background uses --color-brand-1, while the text color needs to contrast with that background. Instead of creating a completely new affiliation, you extend the existing variable:

--color-brand-1 /* Button background */
--color-brand-1-c /* Button text (contrast) */

This relationship-based approach keeps related colors together in your codebase and makes it clear how different colors should be used together.

Additional relationships can include hover states, active states, or variations:

--color-brand-1-1 /* First variation of brand-1 */
--color-brand-1-2 /* Second variation */
--color-brand-1-c /* Contrast color for brand-1 */

The key is maintaining consistency--once you establish a relationship pattern, apply it uniformly across your color system.

The CSS Custom Properties Prefix

While not strictly part of the AIR structure, prefixing color variables with --color- serves an important purpose: it distinguishes color variables from other types of custom properties in your codebase.

As your CSS grows, you'll likely define custom properties for spacing, typography, animations, and other design tokens. The --color- prefix makes it immediately clear which variables represent colors, improving code readability and making it easier to find and manage color definitions.

This prefix also signals to other developers that the variable contains a color value, even if they aren't familiar with your specific naming convention.

/* Color variables - clearly identifiable */
--color-brand-1
--color-site-text
--color-success-1

/* Non-color variables - different prefix */
--spacing-md
--font-size-lg
--transition-fast
Modern CSS Features for Color Theming

While naming conventions provide structure, modern CSS features make implementing robust theming systems more powerful and maintainable than ever before.

Relative Color Syntax

Create new colors by modifying existing ones using oklch() and other color spaces with perceptual uniformity.

color-mix() Function

Blend two colors together with precise control over the mixing ratio in a specified color space.

color-scheme Property

Signal support for light and dark modes, enabling automatic browser adaptation to system preferences.

light-dark() Function

Define both light and dark mode values in a single declaration for automatic theme switching.

Relative Color Syntax with oklch()

The CSS relative color syntax allows you to create new colors by modifying existing ones, opening up sophisticated color manipulation techniques.

The oklch() color space is particularly well-suited for this purpose because it provides perceptual uniformity--changes to one channel (like lightness or chroma) produce predictable, visually consistent results. This means when you adjust a color's lightness, you don't get unexpected shifts in perceived saturation or hue.

:root {
 --base-color: oklch(43.7% 0.075 224);

 /* Darken the base color by 15% */
 --action-color: oklch(from var(--base-color) calc(l * 0.85) c h);

 /* Lighten the action color by 15% for hover state */
 --action-color-light: oklch(from var(--action-color) calc(l * 1.15) c h);
}

The syntax uses the from keyword to reference an existing color and modify specific channels using calc() or other mathematical operations. Channels you don't explicitly modify (represented by their single-letter codes) retain their original values.

Creating Color Harmonies with Relative Colors

You can use relative color syntax to create mathematically harmonious color relationships. A triadic color scheme, for example, keeps the same lightness and chroma values while shifting the hue by 120 degrees in each direction:

:root {
 --base-color: oklch(43.7% 0.075 224);

 /* Triadic colors at +120 and -120 degrees */
 --triadic-primary: oklch(from var(--base-color) l c calc(h + 120));
 --triadic-secondary: oklch(from var(--base-color) l c calc(h - 120));
}

This approach ensures that all colors in your palette share consistent perceptual properties, creating a more harmonious visual experience.

Other harmonic relationships you can create:

  • Complementary: Add 180 degrees to the hue
  • Analogous: Shift hue by 30 degrees in either direction
  • Split-complementary: Add 150 and 210 degrees to the hue

The color-mix() Function

For situations where you want to blend two colors together, the color-mix() function provides an intuitive interface. By specifying a color space (oklab is recommended for perceptual consistency), you can create mixed colors with predictable results:

:root {
 --base-mix-grey-50: color-mix(in oklab, var(--base-color), grey);
 --border-color: var(--base-mix-grey-50);
}

The function accepts percentage values to control the ratio of each color, allowing you to fine-tune the blend:

:root {
 --background-mix-base-80: color-mix(in oklab, var(--background-color) 80%, var(--base-color));
 --surface-light: var(--background-mix-base-80);
}

The first value specifies how much of the first color to include (80% in this case), while the second color fills the remainder.

Implementing Dark Mode with color-scheme

The color-scheme property tells the browser which color themes your site supports, enabling automatic adaptation to system preferences:

:root {
 color-scheme: light dark;
}

Setting this property on the :root element signals to the browser that your site works well in both light and dark modes. The browser can then adjust its default colors for form controls, scrollbars, and other browser-provided UI elements to match the user's preferred color scheme.

For earlier signaling, you can also include a meta tag in your document head:

<meta name="color-scheme" content="light dark">

This meta tag allows browsers to apply the correct color scheme even before your CSS loads, preventing flash-of-wrong-theme.

The light-dark() Function

The light-dark() function provides an elegant way to define both light and dark mode colors in a single declaration:

:root {
 color-scheme: light dark;

 --base-color: light-dark(
 oklch(43.7% 0.075 224),
 oklch(72% 0.1 230)
 );

 --background-color: light-dark(
 oklch(95.5% 0 162),
 oklch(22.635% 0.01351 291.83)
 );

 --text-color: light-dark(
 black,
 white
 );
}

The browser automatically selects the appropriate color based on the current color scheme, eliminating the need for media queries or JavaScript-based theme switching. This makes dark mode implementation significantly simpler and more maintainable.

Practical Implementation

Combining the AIR naming convention with modern CSS features creates a powerful theming system. Here's how you might structure a complete color system:

:root {
 /* CSS color scheme support */
 color-scheme: light dark;

 /* Brand colors */
 --color-brand-1: light-dark(
 oklch(43.7% 0.075 224),
 oklch(72% 0.1 230)
 );
 --color-brand-1-c: light-dark(
 oklch(98% 0 0),
 oklch(85% 0.02 230)
 );
 --color-brand-1-hover: oklch(from var(--color-brand-1) calc(l * 0.9) c h);

 /* Site colors */
 --color-site-bg: light-dark(
 oklch(98% 0 0),
 oklch(15% 0.01 280)
 );
 --color-site-bg-alt: light-dark(
 oklch(95% 0 0),
 oklch(22% 0.015 280)
 );
 --color-site-text: light-dark(
 oklch(15% 0 0),
 oklch(95% 0 0)
 );

 /* Semantic colors */
 --color-success-1: oklch(65% 0.15 140);
 --color-success-1-c: oklch(98% 0.05 140);

 --color-error-1: oklch(60% 0.15 25);
 --color-error-1-c: oklch(98% 0.05 25);
}

Benefits of This Approach

BenefitDescription
ConsistencyEvery color follows the same naming pattern
DiscoverabilityDevelopers can find colors by affiliation
MaintainabilityRebranding requires changing values, not names
Theme supportLight and dark modes are handled automatically
PerformanceCSS custom properties enable efficient runtime changes

A well-structured color system like this works hand-in-hand with a comprehensive design system to ensure visual consistency across all touchpoints.

Common Pitfalls and Best Practices

Avoid Over-Engineering

While the AIR convention provides structure, don't create overly complex variable hierarchies. If you find yourself writing variables like --color-brand-1-2-1-c-5, you've gone too far. The goal is clarity, not complexity.

Document Your System

Even with a clear naming convention, document your color system. Create a style guide that explains your affiliations, shows example values, and demonstrates proper usage. This documentation helps new team members onboard quickly and ensures consistency across the codebase.

Test Color Combinations

Always test your color combinations for accessibility. Use tools like the WebAIM contrast checker to verify that your contrast colors meet WCAG guidelines (minimum 4.5:1 for normal text, 3:1 for large text).

Consider CSS Preprocessors

The AIR convention works with any CSS custom properties system, including Sass variables or CSS-in-JS solutions. The naming approach provides value regardless of your technology choice--only the variable syntax differs.

Building accessible, themeable interfaces is just one aspect of creating exceptional user experiences. Our front-end development services focus on building scalable, maintainable codebases that stand the test of time. For teams looking to master CSS fundamentals, our guide on how to use CSS calc provides additional insights into creating flexible, adaptive layouts.

Conclusion

Naming colors thoughtfully pays dividends throughout a project's lifetime. By shifting from value-based names (like --ocean-blue) to purpose-based names following the AIR convention (like --color-brand-1), you create systems that can evolve without confusion.

Combined with modern CSS features like relative color syntax, color-mix(), and light-dark(), you have a powerful toolkit for creating maintainable, themeable color systems. The initial investment in establishing consistent patterns pays off in reduced maintenance burden, clearer code, and smoother adaptations to changing design requirements.

Remember: the best color names are the ones that still make sense five years from now, even when your design has evolved. Start implementing these patterns in your next project and enjoy the long-term benefits of a well-organized color system.

Frequently Asked Questions

Why should I use numbers instead of words like 'primary' for color IDs?

Numbers are universally understood, easy to sequence, and don't become awkward after a few colors. Words like 'tertiary' and 'quaternary' are rarely used in everyday language and can be confusing for non-native speakers. Numbers also scale naturally--just add the next number in sequence.

Do I have to use the --color- prefix?

The prefix isn't strictly required by the AIR convention, but it's highly recommended. It distinguishes color variables from other custom properties (like spacing or typography) and makes your codebase more readable and maintainable.

Can I use the AIR convention with CSS preprocessors like Sass?

Absolutely. The AIR convention is about naming strategy, not technology choice. You can apply the same patterns with Sass variables, CSS-in-JS, or any other CSS authoring method. Only the variable syntax changes.

How do I handle very complex color systems with many variations?

If you find yourself creating deeply nested variations (like --color-brand-1-2-1-c-5), it's a sign your system may be over-engineered. Consider simplifying by reducing the number of colors or using more descriptive affiliation names to group related colors together.

What's the minimum contrast ratio I should aim for?

For normal text, WCAG requires a contrast ratio of at least 4.5:1. For large text (18px bold or 24px regular), the minimum is 3:1. Always test your contrast colors using tools like WebAIM's contrast checker to ensure accessibility compliance.

Ready to Build a Scalable Design System?

Our web development team specializes in creating maintainable CSS architectures and design systems that grow with your business.