· code · 10 min read
Procedural Color Schemes The Easy Way With HSL
Even programmers with no artistic talent will have to deal with colors/themes on occasion. Let's explore a few simple ways to generate color schemes based on color theory.
This isn’t meant to be deep dive into color theory or themeing but rather some simple examples of how to generate color schemes based on color theory and a little math.
You’ll often see colors represented in CSS as hex values, rgb, or rgba. But, there’s another way to represent colors that’s more intuitive and easier to work with programmatically.
HSL
HSL stands for hue, saturation, and lightness. It’s a representation of the color wheel. Hue is the color value represented as an angle(degrees) on the color wheel(0-360). Saturation is the intensity of the color represented as a percentage(100% is fully saturated) and lightness, also represented as a percentage(100% is full brightness), is the brightness of the color. Optionally you can also specify an alpha value for transparency represented as a value from 0 to 1 (HSLA).
What’s really cool about HSL is the color value(hue) because it represents the color wheel. This opens the possibility of easily generating color themes.
Based on the chart above, you can see how the hue value represents the color wheel. For instance, red is 0 degrees, green is 120 degrees, blue is 240 degrees, etc. Knowing this, we can easily pick a base color( find it’s hue value) and then add the appropriate offset to get the secondary colors.
Formulas
A typical color scheme will have a base color and one or more secondary colors generally fitting one of the main color schemes such as complementary, split complementary, triadic, tetradic, analogous, or monochromatic.
The following formulas will allow you create common color schemes based on color theory.
I’ll be using JavaScript for the examples but the concepts are the same regardless of language
Complementary
/**
* Complementary colors are colors that are opposite each other on the color wheel.
* Base hue + 180 degrees.
* For example, red and green, blue and orange, yellow and purple.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the complementary colors.
*/
const complementary = (hue) => {
return [hue, (hue + 180) % 360];
};
Split Complementary
/**
* Split complementary colors are colors that are the base hue and the two secondary colors that flank its complement.
* Base hue + 150 and base hue + 210 degrees.
* For example, red and blue-green, blue and yellow-orange, yellow and purple-blue.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the split complementary colors.
*/
const splitComplementary = (hue) => {
return [hue, (hue + 150) % 360, (hue + 210) % 360];
};
Triadic
/**
* Triadic colors are colors that are evenly spaced around the color wheel. Generally 3 colors.
* Base hue + 120 and base hue + 240 degrees.
* For example, red and blue and yellow.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the triadic colors.
*/
const triadic = (hue) => {
return [hue, (hue + 120) % 360, (hue + 240) % 360];
};
Tetradic
/**
* Tetradic colors are colors that are evenly spaced around the color wheel. Generally 4 colors.
* Base hue + 60 and base hue + 180 and base hue + 240 degrees.
* For example, red and blue and yellow and green.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the tetradic colors.
*/
const tetradic = (hue) => {
return [hue, (hue + 60) % 360, (hue + 180) % 360, (hue + 240) % 360];
};
Analogous
/**
* Analogous colors are colors that are next to each other on the color wheel.
* Base hue + 30 and base hue + 330 degrees.
* For example, red and orange and yellow.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the analogous colors.
*/
const analogous = (hue) => {
return [hue, (hue + 30) % 360, (hue + 330) % 360];
};
Monochromatic
/**
* Monochromatic colors are colors that are the same hue but different saturation and lightness. However, I like to shift the hue slightly to create a more interesting color scheme.
* Base hue + 10 and base hue + 350 degrees.
* For example, red and orange and yellow.
* @param {number} hue - The hue value of the base color. 0-360
* @returns {number[]} - An array of two hue values representing the analogous colors.
*/
const monochromatic = (hue) => {
return [hue, (hue + 10) % 360, (hue + 350) % 360];
};
Using CSS variables
Now that we can see how this works, let’s do it another way.
The following CSS variables will allow you to create a color scheme based on a base hue. You can then use these variables in your CSS to create a color scheme.
:root {
--hue: 200;
--saturation: 100%;
--lightness: 50%;
--base: hsl(var(--hue), var(--saturation), var(--lightness));
/* you'll probably only use one of these */
--complementary: hsl(calc(var(--hue) + 180), var(--saturation), var(--lightness));
--split-complementary-1: hsl(calc(var(--hue) + 150), var(--saturation), var(--lightness));
--split-complementary-2: hsl(calc(var(--hue) + 210), var(--saturation), var(--lightness));
--triadic-1: hsl(calc(var(--hue) + 120), var(--saturation), var(--lightness));
--triadic-2: hsl(calc(var(--hue) + 240), var(--saturation), var(--lightness));
--tetradic-1: hsl(calc(var(--hue) + 60), var(--saturation), var(--lightness));
--tetradic-2: hsl(calc(var(--hue) + 180), var(--saturation), var(--lightness));
--tetradic-3: hsl(calc(var(--hue) + 240), var(--saturation), var(--lightness));
--analogous-1: hsl(calc(var(--hue) + 30), var(--saturation), var(--lightness));
--analogous-2: hsl(calc(var(--hue) + 330), var(--saturation), var(--lightness));
/* I like to shift the hue slightly to create a more interesting monochromatic color scheme */
--monochromatic-1: hsl(calc(var(--hue) + 10), var(--saturation), var(--lightness));
--monochromatic-2: hsl(calc(var(--hue) + 350), var(--saturation), var(--lightness));
}
/* example usage - split-complementary scheme*/
.bg-primary {
background-color: var(--base);
}
.bg-secondary {
background-color: var(--split-complementary-1);
}
.bg-accent {
background-color: var(--split-complementary-2);
}
This method can be quite powerful and flexible and will suite a lot of use cases. But, what if you want to generate a color scheme dynamically? For instance, if you want to create a color scheme based on a user’s avatar or a random color.
/**
* Generates a color scheme hues based on a base hue that is offset by a specified amount.
* @param {number} baseHue - The base hue value. 0-360
* @param {number} offset - The offset value. 0-360
* @param {number} numColors - The number of colors to generate.
* @returns {number[]} - An array of hue values representing the color scheme.
*/
*/
const generateHues = (baseHue, offset, numColors) =>{
const colors = [];
for (let i = 0; i < numColors; i++) {
colors.push((baseHue + offset * i) % 360);
}
return colors;
}
const genComplementaryHues = (baseHue) => {
return generateHues(baseHue, 180, 2);
};
const genSplitComplementaryHues = (baseHue) => {
return generateHues(baseHue, 150, 3);
};
const genTriadicHues = (baseHue) => {
return generateHues(baseHue, 120, 3);
};
const genTetradicHues = (baseHue) => {
return generateHues(baseHue, 60, 4);
};
const genAnalogousHues = (baseHue) => {
return generateHues(baseHue, 30, 3);
};
const genMonochromaticHues = (baseHue) => {
return generateHues(baseHue, 10, 2);
};
/**
* Utility functions to get relative hues - for instance if you need a blueish tone that matches your base color. In this example the color you get may not be blue but it will be a blueish tone relative to the base color.
*/
const colorWheel = {
blue: 120,
green: 240,
red: 0,
}
/**
* calculate a blueish hue relative to the base hue - pull the base hue towards blue by a strength value
*/
const getRelativeBlueHue = (baseHue, strength = 0.5) => {
const hue = baseHue + (colorWheel.blue - baseHue) * strength;
return hue % 360;
}
/**
* calculate a greenish hue relative to the base hue - pull the base hue towards green by a strength value
*/
const getRelativeGreenHue = (baseHue, strength = 0.5) => {
const hue = baseHue + (colorWheel.green - baseHue) * strength;
return hue % 360;
}
/**
* calculate a reddish hue relative to the base hue - pull the base hue towards red by a strength value
*/
const getRelativeRedHue = (baseHue, strength = 0.5) => {
const hue = baseHue + (colorWheel.red - baseHue) * strength;
return hue % 360;
}
Next we’ll need a function for each scheme that will add the saturation and lightness values to the hues to create the colors. Depending on what type of scheme you want to create, you can do this in a variety of ways.
Let’s create a function that will take a chaos parameter that will determine how much the saturation and lightness values will vary. The higher the chaos, the more variation. For this example we’ll return an array of CSS variables.
/**
* Generates a complementary color scheme based on a base hue and a chaos value.
* @param {number} baseHue - The base hue value. 0-360
* @param {number} chaos - The chaos value. 0-100
* @param {string} varPrefix - The CSS variable prefix. Defaults to 'complementary'
* @returns {string[]} - An array of hsl colors as CSS variables representing the color scheme.
*/
const genComplementaryScheme = (baseHue, chaos, varPrefix = 'complementary') => {
const hues = genComplementaryHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
const genSplitComplementaryScheme = (baseHue, chaos, varPrefix = 'split-complementary') => {
const hues = genSplitComplementaryHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
const genTriadicScheme = (baseHue, chaos, varPrefix = 'triadic') => {
const hues = genTriadicHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
const genTetradicScheme = (baseHue, chaos, varPrefix = 'tetradic') => {
const hues = genTetradicHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
const genAnalogousScheme = (baseHue, chaos, varPrefix = 'analogous') => {
const hues = genAnalogousHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
const genMonochromaticScheme = (baseHue, chaos, varPrefix = 'monochromatic') => {
const hues = genMonochromaticHues(baseHue);
const colors = hues.map((hue, index) => {
const saturation = Math.floor(Math.random() * chaos);
const lightness = Math.floor(Math.random() * chaos);
return `--${varPrefix}-${index}: hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
return colors;
};
Hopefully, you can see how simple this is. The examples above are just for demonstration but understanding the principle, you can formulate your own solutions to fit your needs. Even though I’m using fixed offsets to fit common color theory, there are no rules. You can devise any formula you want and I’d encourage you to experiment.
Note: The code examples above are meant for demonstration and are completely untested. Use at your own risk. If I’ve made a mistake, please let me know