Skip to main content
Back to Blog frontend

Building a Design System with Modern CSS Color Functions

Tom Hermans

Table Of Contents

Building a Complete Design System with Modern CSS Color Functions

Creating a comprehensive design system used to require preprocessing tools, JavaScript, or massive CSS files with hundreds of color variations. Not anymore. Modern CSS gives us color-mix(), light-dark(), relative color syntax, and system colors that work together to create fully dynamic, theme-aware design systems with surprisingly little code.

There are numerous color systems out there, but most of them lack imho

  1. good contrast
  2. easy to switch
  3. computational

The Foundation: System Colors

This approach starts with CSS system colors like Canvas and CanvasText. These aren’t arbitrary values; they’re colors provided by the operating system that automatically adapt to light and dark modes. Canvas is the background, CanvasText is the text color. They’re guaranteed to have proper contrast.

What makes this powerful is that you’re not hardcoding colors. When a user switches their system to dark mode, Canvas and CanvasText flip automatically. Your design system inherits this behavior for free.

Enhancing System Colors with Relative Color Syntax

Here’s where it gets sophisticated. Using the oklch() color space with relative color syntax, we can manipulate system colors while maintaining their light/dark mode awareness. The syntax oklch(from Canvas l c h) takes the Canvas color and lets you modify its lightness, chroma, or hue independently.

This demo adds custom hue and chroma values to system colors, creating tinted variations that still adapt to the user’s theme preference. Want a warmer interface? Add some red hue. Need more saturation? Bump up the chroma. The system colors adjust automatically.

The color-mix() Function

For creating color variations like surfaces and borders, we use color-mix(). This function blends two colors in a specified color space. For example, color-mix(in oklch, Canvas 97%, CanvasText 3%) creates a surface color by mixing mostly Canvas with a tiny bit of CanvasText.

This approach creates related colors that maintain relationships across themes. In light mode, this might be a slightly gray white. In dark mode, it becomes a slightly lighter black. The relationship is preserved, the exact values adapt.

OKLCH Color Space

The demo uses oklch throughout, and that’s intentional. Unlike RGB or HSL, OKLCH is perceptually uniform. When you adjust lightness values, the perceived brightness changes consistently. When you increase chroma, saturation increases predictably.

For design systems, this consistency matters. Your color scales will look balanced, your transitions will feel smooth, and your variations will be visually harmonious in ways that RGB-based approaches struggle to achieve.

The light-dark() Function

For brand colors that need specific values in light and dark modes, we use light-dark(). This function returns the first value in light mode and the second value in dark mode. It’s perfect for accent colors that shouldn’t be derived from system colors.

The syntax is beautifully simple: light-dark(oklch(0.7 0.18 190), oklch(0.6 0.18 190)). Different lightness values for each mode, but the same hue and chroma (or mute the chroma for dark mode perhaps, your choice). Your brand color maintains its identity while being optimized for each context.

Building a Color Hierarchy

The demo creates a complete hierarchy: surface-1 is the base Canvas, surface-2 is elevated, surface-3 is higher elevation. Each level adds more CanvasText to the mix, creating depth through contrast. This same technique applies to text colors, going from primary (full CanvasText) to tertiary (heavily mixed with Canvas).

Borders follow a similar pattern with subtle, default, and strong variations. Each serves a purpose in the visual hierarchy, and all adapt automatically to light and dark modes.

The Theme Override System

Beyond automatic system theme detection, the demo includes manual theme controls. A data-theme attribute on the html element can force light or dark mode, overriding the user’s system preference. This is useful for theme switchers or per-component theming.

The color-scheme CSS property also gets set, ensuring form controls and scrollbars match the chosen theme. It’s a holistic approach where everything coordinates.

Interactive Customization

The demo includes sliders for adjusting hue and chroma in real-time. These update CSS custom properties that feed into the relative color syntax. Moving the hue slider shifts all system colors around the color wheel while maintaining their light/dark mode behavior.

This kind of interactive customization would be nearly impossible with traditional approaches. Here, it’s just updating two custom properties, and the entire design system recalculates.

Practical Components

The demo isn’t just color swatches. It includes real components: buttons, forms, cards, tables, and alerts. Every component uses the design system’s color tokens exclusively. No hardcoded colors, no special cases.

Buttons use brand colors with hover and active states derived using relative color syntax. Forms leverage accent-color to theme checkboxes and radio buttons. Tables and cards use the surface colors for depth. Everything is interconnected.

Accent Color Property

Modern browsers support the accent-color CSS property, which themes form controls like checkboxes, radio buttons, and range inputs. By setting accent-color: var(—brand-base) on the body, all these controls automatically use your brand color.

This is a massive win for design system consistency. No more custom styling for every form element. Just set one property, and your controls match your brand.

The Alert System

Alerts in the demo use color-mix() to create background colors from semantic colors like success green and warning yellow. By mixing these with the base Canvas color, the alerts remain visible and accessible across themes.

The border-left technique provides a bold accent while keeping most of the alert neutral. This approach is both visually distinct and accessible, with proper contrast maintained automatically.

Typography Integration

Typography uses CSS custom properties for size scales and the system font stack for performance. Text colors reference the design system tokens exclusively. Code blocks use surface-3 for background and brand colors for syntax highlighting.

The details/summary elements get themed backgrounds and borders from the system, creating expandable content that feels integrated rather than tacked on.

View on Codepen (opens in a new tab)

Browser Support and Fallbacks

color-mix() is well-supported in modern browsers. Relative color syntax is slightly newer but has good support. For older browsers, you can provide static fallback values before the dynamic ones.

light-dark() is the newest feature, but it degrades gracefully. Browsers that don’t support it will just use their default system colors, which still work fine.

Performance Implications

All these color calculations happen at render time, which is incredibly fast in modern browsers. There’s no runtime JavaScript, no preprocessing step, no build-time generation of thousands of color variants.

The CSS file is compact because you’re defining relationships, not listing every possible color. This approach is actually more performant than traditional design systems with massive color palettes.

Maintainability Benefits

When you need to adjust your brand color, you change one custom property. Everything else updates automatically. Need a new surface elevation? Add one color-mix() declaration. The relationships and logic are explicit in the CSS.

This is dramatically more maintainable than Find & Replace across hundreds of hardcoded hex values. The system is self-documenting and predictable.

Accessibility Built-In

Because the system derives from Canvas and CanvasText, which are guaranteed to have proper contrast, your color hierarchy inherits accessibility. Text on surface colors will be readable because the mixing percentages are calibrated carefully.

The light-dark() approach for brand colors lets you optimize each mode separately. Your dark mode can have different contrast ratios than light mode, ensuring accessibility in both contexts.

Extending the System

Need more color variations? Add more color-mix() declarations. Want semantic colors for errors and warnings? Use oklch() with specific hues. Need a complementary accent? Calculate it using relative color syntax.

The building blocks are there. You can extend this system infinitely while maintaining the same principles and patterns.

Real-World Application

This isn’t a toy demo. This is a production-ready approach to design system color management. It’s used on this very site. The browser support is there, the performance is excellent, and the developer experience is superior. Ie. Headaches are gone trying to wrap your brain around different hues, lightness values, what’s on what, how to color-mix and what mode we’re in.

You get automatic dark mode, dynamic theming, accessible contrast, and infinite extensibility, all with a fraction of the CSS you’d write for traditional color systems.

The Future of Design Systems

This approach represents where design systems are heading. Instead of static tokens managed by build tools, we have dynamic, context-aware systems managed by the browser. Instead of maintaining separate light and dark theme files, we write one set of rules that adapts automatically. Some tokens will still be there, but they more act as a base tokens while calculation and algorithms figure out the rest. (and you can tokenize/variable-ize those too)

CSS has matured to the point where sophisticated design system logic can live entirely in stylesheets. I think this is only scraping the surface and upcoming CSS features like @function will bring even more functionality and flexibility to the table.

Getting Started

To implement this in your own projects, start with system colors as your foundation. Layer in color-mix() for variations and light-dark() for brand colors. Use OKLCH for perceptual uniformity. Add relative color syntax for dynamic adjustments.

Test across themes obsessively. Your light mode might look great while your dark mode has contrast issues. The beauty of this system is you can tune each mode independently while maintaining shared logic.

Final Thoughts

Modern CSS color functions are a quantum leap forward for design systems. This demo shows what’s possible when you combine system colors, color-mix(), light-dark(), and relative color syntax thoughtfully.

The result is a design system that’s dynamic, maintainable, accessible, and performant. It adapts to user preferences automatically while giving you complete control when needed. This is the future of CSS-based design systems, available today.

[Top]

Back to Blog

Let's Talk

Tom avatar

Get in touch

I work at the intersection of design and code.
Interested? Hit me up.
tomhermans@gmail.com

Copyright © 2026 Tom Hermans. Made by Tom Hermans .

All rights reserved 2026 inc Tom Hermans