Skip to content
← All Tools
๐Ÿ”’All processing in your browser ๐ŸšซNo uploads stored ๐Ÿ›ก๏ธPrivacy-first conversion tools โœ“No login required
Tutorial

Building a Design System with CSS Custom Properties and HSL

Bill Crawford — Developer Guide — 2026  ยท  Last updated October 08, 2025

A design system lives or dies by its color palette. Too many hardcoded color values, and changing your brand color requires a find-and-replace across hundreds of files. Too little structure, and developers make inconsistent color decisions that accumulate into visual noise. The combination of HSL color values and CSS custom properties gives you a color system that's consistent, themeable, and almost self-documenting.

Connect on LinkedIn โ†’

Find your perfect colors: Use our RGB/HEX/HSL Converter to get the HSL values for any hex color from your brand guidelines.

Open Color Converter โ†’

Table of Contents

  1. Why HSL for Design Systems
  2. Defining Your Color Scale
  3. Semantic Token Layer
  4. Dark Mode in 10 Lines
  5. Neutral Colors
  6. Checking Contrast
  7. Moving Beyond HSL: oklch in 2026

Why HSL for Design Systems

HSL (Hue, Saturation, Lightness) maps to how designers think about color. When a designer says "make that button slightly darker on hover", that's a lightness change. When they say "use a muted version of the brand color for disabled states", that's a saturation change. With HSL, you can implement these instructions by changing a single number. With HEX or RGB, you'd need to know the new exact value.

Defining Your Color Scale

Start by picking your brand hue (0โ€“360) and building a scale of 9โ€“11 lightness values:

:root {
  /* Brand hue and saturation โ€” single source of truth */
  --brand-h: 221;
  --brand-s: 83%;

  /* Full lightness scale */
  --brand-50:  hsl(var(--brand-h), var(--brand-s), 97%);
  --brand-100: hsl(var(--brand-h), var(--brand-s), 93%);
  --brand-200: hsl(var(--brand-h), var(--brand-s), 85%);
  --brand-300: hsl(var(--brand-h), var(--brand-s), 74%);
  --brand-400: hsl(var(--brand-h), var(--brand-s), 62%);
  --brand-500: hsl(var(--brand-h), var(--brand-s), 53%);  /* base */
  --brand-600: hsl(var(--brand-h), var(--brand-s), 45%);
  --brand-700: hsl(var(--brand-h), var(--brand-s), 37%);
  --brand-800: hsl(var(--brand-h), var(--brand-s), 28%);
  --brand-900: hsl(var(--brand-h), var(--brand-s), 19%);
  --brand-950: hsl(var(--brand-h), var(--brand-s), 12%);
}

This gives you a full palette generated from one hue and one saturation value. Change --brand-h and the entire system recolors. This is the approach Tailwind CSS, Radix UI, and most modern design systems use.

Semantic Token Layer

The scale is your raw palette. Your semantic tokens give these values meaning:

:root {
  /* Semantic tokens reference scale tokens */
  --color-primary:          var(--brand-500);
  --color-primary-hover:    var(--brand-600);
  --color-primary-active:   var(--brand-700);
  --color-primary-subtle:   var(--brand-100);
  --color-primary-on:       white;  /* text on primary background */

  --color-bg:               white;
  --color-bg-raised:        var(--brand-50);
  --color-border:           var(--brand-200);
  --color-text:             var(--brand-900);
  --color-text-muted:       var(--brand-600);
}

/* Components use semantic tokens, not scale tokens */
.btn-primary {
  background: var(--color-primary);
  color: var(--color-primary-on);
  border: 1px solid var(--color-primary);
}
.btn-primary:hover {
  background: var(--color-primary-hover);
}

Dark Mode in 10 Lines

Because everything references semantic tokens, dark mode is just redefining those tokens:

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg:          var(--brand-950);
    --color-bg-raised:   var(--brand-900);
    --color-border:      var(--brand-800);
    --color-text:        var(--brand-50);
    --color-text-muted:  var(--brand-400);
    --color-primary:     var(--brand-400);  /* lighter in dark mode */
    --color-primary-hover: var(--brand-300);
  }
}

No component styles need to change. Every element that uses semantic tokens automatically updates.

Neutral Colors

Most design systems need a neutral (grey) scale alongside the brand color. Use the same HSL approach with very low saturation โ€” a "warm grey" keeps a tiny bit of the brand hue:

:root {
  --neutral-h: 221;
  --neutral-s: 10%;  /* very low saturation = near-grey with warm tint */

  --neutral-100: hsl(var(--neutral-h), var(--neutral-s), 97%);
  --neutral-500: hsl(var(--neutral-h), var(--neutral-s), 55%);
  --neutral-900: hsl(var(--neutral-h), var(--neutral-s), 12%);
  /* ... full scale */
}

Checking Contrast

A color system is only as good as its accessibility. For every text-on-background combination, verify the contrast ratio meets WCAG AA (4.5:1 for normal text, 3:1 for large text). Lightness in HSL gives you a rough guide โ€” text that's 90+ lightness on a background that's 10 lightness will always pass, but mid-range values need checking. Use a contrast checker to validate your semantic token pairings before shipping.

Moving Beyond HSL: oklch in 2026

HSL has been the go-to color space for design systems since CSS3. But it has a known weakness: equal lightness values in HSL do not look equally bright perceptually. Yellow at hsl(60, 100%, 50%) appears much lighter than blue at hsl(240, 100%, 50%).

oklch solves this. It is a perceptually uniform color space where equal L values genuinely look equally bright, regardless of hue. All major browsers have supported it since 2023. For new design systems or major token updates, oklch is now the recommended choice:

/* HSL โ€” old approach */
:root {
  --color-primary: hsl(220, 80%, 55%);
  --color-primary-light: hsl(220, 80%, 75%);
  --color-primary-dark: hsl(220, 80%, 35%);
}

/* oklch โ€” modern approach (perceptually consistent) */
:root {
  --hue: 250;
  --color-primary: oklch(55% 0.18 var(--hue));
  --color-primary-light: oklch(75% 0.12 var(--hue));
  --color-primary-dark: oklch(35% 0.22 var(--hue));
}

The oklch approach makes it trivially easy to create accessible color palettes โ€” all tones at the same L value will appear equally bright, making contrast ratio calculations more predictable. For teams starting a new design system in 2025+, oklch is the recommended color space.

BC
Bill Crawford
Founder, Data Conversion Center

Bill Crawford is a data systems developer and technical founder with over 30 years of professional experience in accounting, finance, and business operations.

He holds a Bachelor's degree in Accounting and has spent more than three decades working within financial and operational environments. Over the past 10 years, he has been heavily involved in the development, implementation, and refinement of financial and enterprise data systems for both Fortune 500 companies and smaller organizations.

His work bridges finance and technology — combining deep domain knowledge in structured reporting and accounting workflows with hands-on SQL development and database architecture experience.

Bill founded DataConversionCenter.com to build practical, browser-based tools that simplify complex data challenges, including:

Rather than focusing on theoretical examples, his tools and articles are informed by real-world challenges encountered in enterprise reporting systems, financial databases, and operational data environments.

Professional Background

Bill's mission is to reduce friction in data workflows — particularly for professionals working with structured financial, operational, and reporting data.