Generate a color palette with CSS
In this blog, I will explain how I use the CSS color function, color() and relative values to generate a color palette.
In this blog, I will explain how I use the CSS color function, color() and relative values to generate a color palette.
From this article I learned how to use the color() function to modify colors.
What it does is take a color, and then you can adjust its individual values.
For the HSL color function, the syntax looks like this:
hsl(from 'origin-color' h s l)If you write out H, S, and L, the values stay the same. If you replace one with a number, only that value changes to the new number. For example, here I change the hue, which alters my color:
background-color: hsl(from #9333ea 20 s l);If you want to create a lighter or darker version of the same color, you just need to adjust the lightness value:
/* Darker */
background-color: hsl(from #9333ea h s calc(l - 20));
/* Lighter*/
background-color: hsl(from #9333ea h s calc(l + 20));Here, I used calc() to calculate how much lighter or darker the L value should be.
1 = the original value, and we add or subtract from it to make it lighter or darker.
You might have noticed, but now we’re passing a hex color code and changing the lightness value. How is that possible? The hsl() color function automatically converts the color to HSL and then changes the L value.
This also works for colors in other color formats:
/* hsl */
background-color: hsl(from hsl(201, 72%, 18%) h s calc(l + 20));
/* oklch */
background-color: hsl(from oklch(69.58% 0.16 55.54) h s calc(l + 20));We can do this with hsl(), but also with oklch()
for example:
background-color: oklch(from #9333ea calc(l + 20) c h);In HSL, the browser shows srgb when you hover over the color, but in OKLCH, it actually shows oklch
OKLCH has more values between colors, which makes lighter and darker variations look nicer because they don’t become too bright or too dark. In this blog/tutorial, I will continue using OKLCH.
Writing out the color function everywhere in a project is a lot of work and hard to maintain. With custom properties, we can make it easier. For example:
:root {
--primary: oklch(32.79% 0.063 237.53);
--primary-light: oklch(from var(--primary) calc(l + 0.1) c h);
}
div {
background-color: var(--primary-light);With this approach, you can use the colors much more easily, and if you change the --primary color, all color variations will update automatically.
If we use this approach, a lot of repetition occurs, which becomes noticeable once you start adding more colors or color variations.
:root {
--primary: oklch(32.79% 0.063 237.53);
--secondary: oklch(0.6073 0.1939 31.79);
--neutral: oklch(0.7 0.2358 326.14);
--primary-dark-2: oklch(from var(--primary) calc(l - 0.1) c h);
--primary-dark-1: oklch(from var(--primary) calc(l - 0.05) c h);
--primary-light-1: oklch(from var(--primary) calc(l + 0.1) c h);
--primary-light-2: oklch(from var(--primary) calc(l + 0.2) c h);
--secondary-dark-2: oklch(from var(--secondary) calc(l - 0.1) c h);
--secondary-dark-1: oklch(from var(--secondary) calc(l - 0.05) c h);
--secondary-light-1: oklch(from var(--secondary) calc(l + 0.1) c h);
--secondary-light-2: oklch(from var(--secondary) calc(l + 0.2) c h);
--neutral-dark-2: oklch(from var(--neutral) calc(l - 0.1) c h);
--neutral-dark-1: oklch(from var(--neutral) calc(l - 0.05) c h);
--neutral-light-1: oklch(from var(--neutral) calc(l + 0.1) c h);
--neutral-light-2: oklch(from var(--neutral) calc(l + 0.2) c h);
}Fortunately, there’s a solution for this using only CSS:
:root {
--primary: oklch(32.79% 0.063 237.53);
--secondary: oklch(0.6073 0.1939 31.79);
--neutral: oklch(0.7 0.2358 326.14);
}
.primary,
.secondary,
.neutral {
--dark-2: oklch(from var(--color) calc(l - 0.1) c h);
--dark-1: oklch(from var(--color) calc(l - 0.05) c h);
--light-1: oklch(from var(--color) calc(l + 0.1) c h);
--light-2: oklch(from var(--color) calc(l + 0.2) c h);
}
.primary {
--color: var(--primary);
}
.secondary {
--color: var(--secondary);
}
.neutral {
--color: var(--neutral);
}<ul class="primary">
<li><h2>Primary</h2></li>
<li><div></div></li>
<li><div></div></li>
<li><div></div></li>
<li><div></div></li>
<li><div></div></li>
</ul>This looks exactly the same in the browser as before, but in the code, there are three disadvantages:
--dark-2, --dark-1, --light-1, --light-2 zijn, the class in the HTML determines which color is displayed.primary to your container, and then within that container, you can use all the variations of primary as well as the main colors: --primary, --secondairy, --neutral.<ul class="primary">
<li><h2>Primary</h2></li>
<li><div></div></li>
<li class="secondary"><div></div></li>
</ul>The one I found was SCSS. Here, you can create a loop to generate all color variants. Each color gets its own custom property, so you don’t have to work with classes. For example:
// Original colors
$colors: (
primary: oklch(32.79% 0.063 237.53),
secondary: oklch(0.6073 0.1939 31.79),
neutral: oklch(0.7 0.2358 326.14)
);
// lighter and darker variants
$variants: (
dark-2: -0.1,
dark-1: -0.05,
light-1: 0.1,
light-2: 0.2
);
// make custom properties for each color variant
:root {
// Take each color in $colors and separate the name and the color code
@each $color-name, $color-value in $colors {
// use name and color code to make a CSS custom property
--#{$color-name}: #{$color-value};
@each $variant-name, $offset in $variants {
// use name + variant name to make a new custom property
// And use color code + variant in color function
--#{$color-name}-#{$variant-name}: oklch(from var(--#{$color-name}) calc(l + #{$offset}) c h);
}
}
}I tried to replicate this loop in Vue and Svelte, but unfortunately, it’s not possible to loop over CSS in those files. A missed opportunity, if you ask me
OKLCH has been supported in the most commonly used browsers, Chrome, Edge, Safari, and Opera since 2023. Since 2025, it’s also supported in Safari and Samsung Internet. Because this is still very recent, we can adjust our code to use HSL by default, and use OKLCH when the browser supports it.
:root {
--primary: #0d3951;
--secondary: #de452d;
--neutral: #e55bea;
}
.primary,
.secondary,
.neutral {
--dark-2: hsl(from var(--color) h s calc(l - 10));
--dark-1: hsl(from var(--color) h s calc(l - 5));
--light-1: hsl(from var(--color) h s calc(l + 10));
--light-2: hsl(from var(--color) h s calc(l + 20));
@supports (color: oklch(32.79% 0.063 237.53)) {
--dark-2: oklch(from var(--color) calc(l - 0.1) c h);
--dark-1: oklch(from var(--color) calc(l - 0.05) c h);
--light-1: oklch(from var(--color) calc(l + 0.1) c h);
--light-2: oklch(from var(--color) calc(l + 0.2) c h);
}
}
.primary {
--color: var(--primary);
}
.secondary {
--color: var(--secondary);
}
.neutral {
--color: var(--neutral);
}What’s happening here?
Relative colors for HSL and OKLCH have been supported in major browsers since 2024. Because this is still recent, we can create a fallback using color-mix.
color-mix works similarly to hsl() and oklch().
It takes a color, and you can apply a tint/filter to it. This tint is another color.
To create lighter and darker variants, I will use black and white.
color-mix can also accept a hex color and convert it to HSL or OKLCH.
/* hsl */
--dark-2: color-mix(in hsl, var(--color) 80%, black 20%);
/* oklch */
--dark-2: color-mix(in oklch, var(--color) 80%, black 20%);Since this is a fallback, I will use HSL in the color-mix:
:root {
--primary: #0d3951;
--secondary: #de452d;
--neutral: #e55bea;
}
.primary,
.secondary,
.neutral {
--dark-2: color-mix(in hsl, var(--color) 80%, black 20%);
--dark-1: color-mix(in hsl, var(--color) 90%, black 10%);
--light-1: color-mix(in hsl, var(--color) 90%, white 10%);
--light-2: color-mix(in hsl, var(--color) 80%, white 20%);
/* comment this @support and the next one to see color-mix() */
@supports (color: hsl(201, 72%, 18%)) {
--dark-2: hsl(from var(--color) h s calc(l - 10));
--dark-1: hsl(from var(--color) h s calc(l - 5));
--light-1: hsl(from var(--color) h s calc(l + 10));
--light-2: hsl(from var(--color) h s calc(l + 20));
}
/* comment this @support to see hsl() */
@supports (color: oklch(32.79% 0.063 237.53)) {
--dark-2: oklch(from var(--color) calc(l - 0.1) c h);
--dark-1: oklch(from var(--color) calc(l - 0.05) c h);
--light-1: oklch(from var(--color) calc(l + 0.1) c h);
--light-2: oklch(from var(--color) calc(l + 0.2) c h);
}
}
.primary {
--color: var(--primary);
}
.secondary {
--color: var(--secondary);
}
.neutral {
--color: var(--neutral);
}oklch():
hsl():
color-mix():
The only downside of color-mix is that it uses percentages to mix two colors, giving you less control over the variants. With hsl() and oklch(), you can specify exactly which values to change.
Since both color-mix() and hsl() return HSL, I would use only color-mix() from these two, and use OKLCH as an enhancement.
If you generate colors this way, it’s very easy to adjust your colors.
Adding new colors is also very simple—you only need this:
:root {
--new-color: #0d3951;
}
.new-color{
--color: var(--new-color);
}Four lines of code give you, (using only color-mix() and OKLCH):
4 OKLCH colors and 4 HSL colors as a fallback
If you were to write everything out manually, you’d end up with twice as many lines of code.
The downside is that you then have to work with classes in HTML and the same custom property for all colors. But you can solve this with a loop in SCSS, which generates all these color variations with their own custom property names.
Codepen:
Can I Use: