Generating UI Design Palettes with Consistent Hsl Lightness Scales

Why do your UI color palettes look inconsistent across components, even when you're starting from the same brand color?

I've been building design systems for SaaS dashboards for the better part of a decade, and I can tell you exactly why this happens. You pick a brand color—say, a confident blue—drop it into your button component, your badges, your alerts, and your hover states. Then you manually lighten it for backgrounds and darken it for borders. Somewhere along the way, things start looking... off. The hover state looks muddy. The background tint looks like it belongs to a different product. The disabled state reads as gray instead of "faded brand color."

The culprit is almost always inconsistent lightness scaling. When you adjust colors by eye or by blindly mixing with white and black, you're not controlling lightness independently of hue and saturation. That's where HSL color conversion becomes your best friend—and where a disciplined approach to generating UI design palettes with consistent HSL lightness scales changes everything.

What is an HSL lightness scale and why does it matter for UI design?

HSL stands for Hue, Saturation, and Lightness. Unlike RGB or hex values—which mix red, green, and blue channels in ways that are hard to reason about visually—HSL mirrors how we actually perceive color. Hue is the color itself (0-360 degrees on the color wheel). Saturation is how intense it is (0-100%). Lightness is how bright or dark it is (0% is black, 100% is white, 50% is pure color).

Here's why that matters for UI work: when you build a palette with a consistent HSL lightness scale, you're creating a ramp where each step is perceptually predictable. Your color-brand-50 is always a pale tint. Your color-brand-500 is always your core brand color. Your color-brand-900 is always a deep shade. The hue stays recognizable at every step because you're only moving one variable—lightness—in a controlled way.

This is the foundation of every great design system palette. Tailwind does it. Material Design does it. Radix UI does it. The question is how you do it for your own brand colors without spending three hours in Figma squinting at swatches.

How do you convert a hex color to HSL for palette generation?

Let's walk through a real example. Say your brand color is #3B82F6—that's Tailwind's blue-500, a color I use constantly in dashboard work.

First, you need to convert that hex to HSL. You can do this with a color conversion tool, or you can do the math yourself. Here's the manual process because understanding it makes you better at catching errors later:

Convert the hex to RGB first: #3B82F6 gives you R=59, G=130, B=246. Normalize those to 0-1 by dividing by 255: R=0.231, G=0.510, B=0.965. Find the max and min values: max=0.965 (blue), min=0.231 (red). The lightness is (max + min) / 2 = (0.965 + 0.231) / 2 = 0.598, which is 59.8%. Round to 60%.

For saturation, since lightness is neither 0 nor 1, you calculate: (max - min) / (1 - |2L - 1|) = (0.965 - 0.231) / (1 - |2(0.598) - 1|) = 0.734 / (1 - 0.196) = 0.734 / 0.804 = 0.913, which is 91.3%. Round to 91%.

For hue, since max is blue: H = 60 × ((R - G) / (max - min) + 4) = 60 × ((0.231 - 0.510) / 0.734 + 4) = 60 × (-0.380 + 4) = 60 × 3.620 = 217.2°. Round to 217°.

So #3B82F6 in HSL is hsl(217, 91%, 60%). That's your anchor. Every color in your palette will share that 217° hue.

Why not just use HSL from the start?

Honestly, you can. If you're defining brand colors in HSL from day one, you skip the conversion step entirely. I've started doing this on newer projects—defining my core palette as HSL values in CSS custom properties and only converting to hex when a stakeholder asks for the "official" brand color code. But most of us inherit hex colors from brand guidelines, so knowing how to convert is non-negotiable.

What lightness values should you use for a consistent UI palette?

This is where most people get it wrong. They think a 10-step palette means lightness values of 10%, 20%, 30%, all the way to 100%. That's even spacing, and it looks terrible. Human perception of lightness is non-linear. The difference between 90% and 95% lightness is visually massive, while the difference between 40% and 45% is barely noticeable.

Here's the lightness scale I've settled on after years of building dashboard UIs. It works for about 90% of brand colors:

  • 50: 97% lightness — barely-there tints for backgrounds
  • 100: 93% — subtle surface tints, table row hover
  • 200: 86% — light badges, selected states
  • 300: 76% — disabled states, soft borders
  • 400: 66% — secondary buttons, icons on light backgrounds
  • 500: 60% — your anchor brand color (this is where your hex lives)
  • 600: 52% — primary button hover
  • 700: 45% — active/pressed states
  • 800: 37% — dark mode surfaces, high-contrast text on light
  • 900: 28% — deepest shade, rarely used but essential for range

Notice that the steps aren't even. The gap between 50 and 100 is 4 percentage points. The gap between 500 and 600 is 8 points. The gap between 800 and 900 is 9 points. This non-linear distribution accounts for how your eyes perceive lightness changes at different points in the scale.

How do you maintain hue consistency across your lightness scale?

Here's the trap that catches even experienced designers. You set your hue to 217° and your saturation to 91%, then you generate all ten steps by only changing lightness. Done, right? Not quite. The problem is that HSL saturation doesn't map evenly to perceived saturation. A color at 97% lightness with 91% saturation looks like a washed-out pastel. A color at 28% lightness with 91% saturation looks almost black.

To get a palette that feels like the same color at every step, you need to adjust saturation as lightness changes. Here's the rule of thumb I use:

  • At very high lightness (90%+): drop saturation to 40-60%. Otherwise, pastels look neon.
  • At high lightness (75-89%): saturation around 70-85% works well.
  • At mid lightness (50-74%): keep saturation close to your anchor value.
  • At low lightness (below 45%): increase saturation to 95-100% to keep the color recognizable as it gets darker.

For our blue example, here's what the adjusted palette looks like:

  • 50: hsl(217, 45%, 97%)
  • 100: hsl(217, 60%, 93%)
  • 200: hsl(217, 78%, 86%)
  • 300: hsl(217, 85%, 76%)
  • 400: hsl(217, 89%, 66%)
  • 500: hsl(217, 91%, 60%) — our anchor
  • 600: hsl(217, 92%, 52%)
  • 700: hsl(217, 94%, 45%)
  • 800: hsl(217, 96%, 37%)
  • 900: hsl(217, 100%, 28%)

See how saturation climbs as lightness drops? That's what keeps the deep shades from looking like generic dark blue and the pale tints from looking like generic light blue. They all read as your blue.

How do you generate CSS variables from your HSL palette?

Once you've got your HSL values locked in, the final step is turning them into something your codebase can actually use. I define every color as a CSS custom property using HSL components separated by spaces, so I can adjust opacity on the fly:

:root {
  --brand-50: 217 45% 97%;
  --brand-100: 217 60% 93%;
  --brand-200: 217 78% 86%;
  --brand-300: 217 85% 76%;
  --brand-400: 217 89% 66%;
  --brand-500: 217 91% 60%;
  --brand-600: 217 92% 52%;
  --brand-700: 217 94% 45%;
  --brand-800: 217 96% 37%;
  --brand-900: 217 100% 28%;
}

.button-primary {
  background-color: hsl(var(--brand-500));
}

.button-primary:hover {
  background-color: hsl(var(--brand-600));
}

.badge-info {
  background-color: hsl(var(--brand-100));
  color: hsl(var(--brand-800));
}

.overlay {
  background-color: hsl(var(--brand-500) / 0.2);
}

That last line is the killer feature. Because I'm storing HSL components without the hsl() wrapper, I can use the modern hsl(H S L / alpha) syntax to create translucent brand-colored overlays, focus rings, and shadows without converting to RGB or rgba. It's the cleanest way I've found to handle opacity in a design system.

What common mistakes break HSL palette consistency?

I've reviewed a lot of design system palettes over the years, and the same issues come up again and again. Here are the ones I see most:

Mixing color spaces mid-palette. Someone generates steps 50-400 in HSL, then switches to mixing with black for the darker shades because it "looks fine." It doesn't. Those dark steps will feel disconnected from the rest of the scale, especially when placed side by side in a data visualization. Pick one color space and commit.

Using pure white and black for tints and shades. Mixing #3B82F6 with white at 20% intervals doesn't produce a consistent palette—it produces a gradient that passes through desaturated middle ground. HSL lightness adjustments keep saturation under your control, which is why they produce better results.

Forgetting to test in dark mode. A palette that looks great on white backgrounds can fall apart on dark surfaces. Your brand-100 tint, designed as a subtle background on light mode, becomes invisible on a dark theme. I always generate a parallel dark-mode palette where the lightness values are inverted and shifted—brand-500 in dark mode might be hsl(217, 91%, 65%) instead of 60%, because colors need to be slightly brighter to read well against dark backgrounds.

Ignoring perceptual uniformity for yellow and green hues. HSL isn't perfect. Yellows (around 50-60°) and greens (around 120-150°) are perceptually brighter than blues and purples at the same lightness value. If your brand color is yellow, you'll need to shift your lightness scale down by about 5-8 percentage points across the board to get the same visual weight. It's an annoying quirk, but once you know about it, you can compensate.

The workflow I've walked you here—convert hex to HSL, define a non-linear lightness scale, adjust saturation per step, output as CSS custom properties—is the same one I use on every project. It takes about 20 minutes once you've done it a few times, and it produces palettes that hold up across components, themes, and accessibility audits. No more muddy hover states. No more disconnected tints. Just a color system that actually feels like a system.

Frequently Asked Questions

How do I generate a consistent UI color palette using HSL?

To generate a consistent UI palette using HSL, keep the Hue (H) and Saturation (S) values constant while systematically adjusting the Lightness (L) percentage. This creates a cohesive scale of tints and shades that visually belong to the same color family. Using a color conversion tool automates this process to ensure mathematically even steps between each shade.

Why is HSL better than RGB for creating UI color scales?

HSL (Hue, Saturation, Lightness) represents color in a way that aligns with human visual perception, making it much easier to predict how a shade will look. RGB mixes light additively, which often results in muddy or unexpected hues when darkening or lightening colors. By adjusting only the Lightness channel in HSL, you guarantee a clean, consistent transition across your UI palette.

What is the ideal lightness scale percentage for UI design systems?

A standard UI design system typically uses a 10-step scale ranging from 10% to 100% lightness, often skipping pure black (0%) and pure white (100%). Common increments include steps like 10, 20, 30, 40, 50, 60, 70, 80, and 90 percent lightness. This provides enough variance to cover backgrounds, borders, hover states, and text while maintaining strict visual consistency.

How do I convert my HSL color palette to HEX codes for CSS?

Once you have generated your HSL lightness scale, you can use a color conversion tool to instantly translate those values into HEX, RGB, or HSL CSS strings. Most modern design tools and online converters allow you to export an entire palette as CSS variables or Tailwind config code. This ensures your mathematically consistent HSL colors are ready for production development.

How do I keep saturation consistent when generating UI color palettes?

When generating a palette, lock your saturation value across all shades of a specific hue. For very light or very dark UI scales, you may need to slightly increase saturation at the extremes to prevent the colors from looking washed out. A robust color conversion tool will allow you to fine-tune these values while keeping the core scale mathematically intact.

How many shades should a UI design color palette include?

Most modern UI design systems include between 9 to 11 shades per primary color to cover all interface needs. This typically includes very light variants for subtle backgrounds, mid-tones for interactive elements, and dark variants for text and borders. Generating exactly 10 steps using an HSL scale provides the perfect balance of flexibility and consistency.

How do I ensure accessible contrast ratios using an HSL lightness scale?

Because HSL lightness directly controls how light or dark a color appears, it makes it much easier to pair colors that meet WCAG contrast requirements. For standard body text, pair a very low lightness percentage with a very high lightness percentage from your scale. You can then use a color conversion tool to verify the exact contrast ratios before exporting to your codebase.

Are there tools to automatically generate HSL color scales for UI?

Yes, many color conversion tools and palette generators allow you to input a base color and automatically generate a 10-point scale using HSL lightness. These tools let you adjust the hue, lock the saturation, and output the exact lightness variations needed for your design system. They often feature one-click exports to CSS, SCSS, or JSON formats.

Why do my UI colors look inconsistent and how can HSL fix it?

UI colors often look inconsistent because they were hand-picked without a mathematical relationship between their underlying RGB values. Switching to an HSL workflow forces you to use the exact same hue and saturation while only shifting lightness, creating a harmonious family of colors. This removes the guesswork and ensures every shade feels like a natural extension of your brand color.

How do I generate a neutral gray scale using HSL for UI design?

To create a UI neutral gray scale, set your Saturation (S) to 0% and systematically step through your Lightness (L) percentages from 10% to 90%. If you want a cool or warm gray, introduce a very small amount of saturation with a specific hue like 240 for blue. This generates a consistent, scalable neutral palette perfect for backgrounds, borders, and typography.