I know you’re here to learn about manipulating colors  —  and we’ll get there. But before we do, we need a baseline understanding of how CSS notates colors. CSS uses two different color models: RGB and HSL. Let’s take a quick look at both.
An initialism for “red, green, blue,” RGB consists of three numbers that each signify how much light of its respective color is included in the resulting end color. In CSS, each of these numbers is in the range of 0–255 and would be written as comma-separated parameters of the CSS rgb
function. For example, rgb(50,100,0)
.
RGB is an “additive” color system, which means that the higher each number is, the brighter the end color will be. If all values are equal, the color will be grayscale; if all values are zero, the result will be black; and if all values are 255, the result will be white.
Alternatively, you can notate RGB colors using the hexadecimal notation, in which each color’s integer is converted from base 10 to base 16. For example, rgb(50,100,0)
would be #326400
.
Although I usually find myself reaching for RGB (particularly hexadecimal) out of habit, I often find that it is hard to read and especially hard to manipulate. Enter HSL.
An initialism for “hue, saturation, light,” HSL also consists of three values. The hue value corresponds to the position on the color wheel and is represented by a CSS angle value; most commonly, deg units are used.
Saturation, represented by a percentage, refers to the intensity of the color. When saturation is 100 percent, it is fully colored; the less saturation, the less color, until it reaches grayscale at 0 percent.
Lightness, also represented by a percentage, refers to how bright a color is. “Regular” brightness is 50 percent. A lightness of 100 percent will be pure white, and 0 percent lightness will be pure black, regardless of the hue and saturation values.
I find HSL to be a more intuitive model. Relations between colors are more immediately evident, and manipulation of colors tends to be as simple as tweaking just one of the numbers.
Both the RGB and HSL color models break down a color into various attributes. To convert between the syntaxes, we first need to calculate these attributes.
With the exception of hue, each value we have discussed can be represented as a percentage. Even the RGB values are byte-sized representations of percentages. In the formulas and functions below, these percentages will be represented by decimals between 0 and 1.
I would like to note that I will not cover the math for these in depth; rather, I will briefly go over the original mathematical formula and then convert it into a JavaScript formula.
Lightness is the easiest of the three HSL values to calculate. Mathematically, the formula is displayed as follows, where M
is the maximum of the RGB values and m
is the minimum:
Here is the same formula as a JavaScript function:
const rgbToLightness = (r,g,b) =>
1/2 * (Math.max(r,g,b) + Math.min(r,g,b));
Saturation is only slightly more complicated than lightness. If the lightness is either 0 or 1, then the saturation value will be 0. Otherwise, it follows the mathematical formula below, where L
represents lightness:
As JavaScript:
const rgbToSaturation = (r,g,b) => {
const L = rgbToLightness(r,g,b);
const max = Math.max(r,g,b);
const min = Math.min(r,g,b);
return (L === 0 || L === 1)
? 0
: (max - min)/(1 - Math.abs(2 * L - 1));
};
The formula for calculating the hue angle from RGB coordinates is a bit more complex:
As JavaScript:
const rgbToHue = (r,g,b) => Math.round(
Math.atan2(
Math.sqrt(3) * (g - b),
2 * r - g - b,
) * 180 / Math.PI
);
The multiplication of 180 / Math.PI
at the end is to convert the result from radians to degrees.
All of these functions can be wrapped into a single utility function:
const rgbToHsl = (r,g,b) => {
const lightness = rgbToLightness(r,g,b);
const saturation = rgbToSaturation(r,g,b);
const hue = rgbToHue(r,g,b);
return [hue, saturation, lightness];
}
Before jumping into calculating RGB, we need a few prerequisite values.
First is the “chroma” value:
We also have a temporary hue value, whose range we will use to decide which “segment” of the hue circle we belong on:
Next, we have an “x” value, which will be used as the middle (second-largest) component value:
We have an “m” value, which is used to adjust each of the values for lightness:
Depending on the hue prime value, the r
, g
, and b
values will map to C
, X
, and 0
:
Lastly, we need to map each value to adjust for lightness:
Putting all of this together into a JavaScript function:
const hslToRgb = (h,s,l) => { const C = (1 - Math.abs(2 * l - 1)) * s; const hPrime = h / 60; const X = C * (1 - Math.abs(hPrime % 2 - 1)); const m = l - C/2; const withLight = (r,g,b) => [r+m, g+m, b+m];
if (hPrime <= 1) { return withLight(C,X,0); } else if (hPrime <= 2) { return withLight(X,C,0); } else if (hPrime <= 3) { return withLight(0,C,X); } else if (hPrime <= 4) { return withLight(0,X,C); } else if (hPrime <= 5) { return withLight(X,0,C); } else if (hPrime <= 6) { return withLight(C,0,X); } }
For ease of access when manipulating their attributes, we will be dealing with a JavaScript object. This can be created by wrapping the previously written functions:
const rgbToObject = (red,green,blue) => { const [hue, saturation, lightness] = rgbToHsl(red, green, blue); return {red, green, blue, hue, saturation, lightness}; }
const hslToObject = (hue, saturation, lightness) => { const [red, green, blue] = hslToRgb(hue, saturation, lightness); return {red, green, blue, hue, saturation, lightness}; }
I highly encourage you to spend some time playing with this example. Seeing how each of the attributes interacts when you adjust the others can give you a deeper understanding of how the two color models fit together.
Now that we have the ability to convert between color models, let’s look at how we can manipulate these colors!
Each of the color attributes we have covered can be manipulated individually, returning a new color object. For example, we can write a function that rotates the hue angle:
const rotateHue = rotation => ({hue, ...rest}) => { const modulo = (x, n) => (x % n + n) % n; const newHue = modulo(hue + rotation, 360);
return { ...rest, hue: newHue }; }
The rotateHue
function accepts a rotation
parameter and returns a new function, which accepts and returns a color object. This allows for the easy creation of new “rotation” functions:
const rotate30 = rotateHue(30); const getComplementary = rotateHue(180);
const getTriadic = color => { const first = rotateHue(120); const second = rotateHue(-120); return [first(color), second(color)]; }
Along the same lines, you can write functions to saturate
or lighten
a color  —  or, inversely, desaturate
or darken
.
const saturate = x => ({saturation, ...rest}) => ({ ...rest, saturation: Math.min(1, saturation + x), });
const desaturate = x => ({saturation, ...rest}) => ({ ...rest, saturation: Math.max(0, saturation - x), });
const lighten = x => ({lightness, ...rest}) => ({ ...rest, lightness: Math.min(1, lightness + x) });
const darken = x => ({lightness, ...rest}) => ({ ...rest, lightness: Math.max(0, lightness - x) });
In addition to color manipulation, you can write “predicates”  —  that is, functions that return a Boolean value.
const isGrayscale = ({saturation}) => saturation === 0;
const isDark = ({lightness}) => lightness < .5;
The JavaScript [].filter
method accepts a predicate and returns a new array with all the elements that “pass.” The predicates we wrote in the previous section can be used here:
const colors = [/* ... an array of color objects ... */];
const isLight = ({lightness}) => lightness > .5;
const lightColors = colors.filter(isLight);
To sort an array of colors, you first need to write a “comparator” function. This function takes two elements of an array and returns a number to denote the “winner.” A positive number indicates that the first element should be sorted first, and a negative indicates the second should be sorted first. A zero value indicates a tie.
For example, here is a function for comparing the lightness of two colors:
const compareLightness = (a,b) => a.lightness - b.lightness;
Here is a function that compares saturation:
const compareSaturation = (a,b) => a.saturation - b.saturation;
In an effort to prevent duplication in our code, we can write a higher-order function to return a comparison function to compare any attribute:
const compareAttribute = attribute => (a,b) => a[attribute] - b[attribute];
const compareLightness = compareAttribute('lightness'); const compareSaturation = compareAttribute('saturation'); const compareHue = compareAttribute('hue');
You can average the specific attributes of an array of colors by composing various JavaScript array methods. First, you can calculate the average of an attribute by summing with reduce and dividing by the array length:
const colors = [/* ... an array of color objects ... */];
const toSum = (a,b) => a + b;
const toAttribute = attribute => element => element[attribute];
const averageOfAttribute = attribute => array =>
array.map(toAttribute(attribute)).reduce(toSum) / array.length;
You can use this to “normalize” an array of colors:
/* ... continuing */
const normalizeAttribute = attribute => array => { const averageValue = averageOfAttribute(attribute)(array); const normalize = overwriteAttribute(attribute)(averageValue); return normalize(array); }
const normalizeSaturation = normalizeAttribute('saturation'); const normalizeLightness = normalizeAttribute('lightness'); const normalizeHue = normalizeAttribute('hue');
Colors are an integral part of the web. Breaking down colors into their attributes allows for the smart manipulation of colors and opens the door to all sorts of possibilities.
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
Build a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend.
console.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.