hsl – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Wed, 19 Oct 2022 14:04:01 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.5 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 hsl – CSS-Tricks https://css-tricks.com 32 32 45537868 A Whistle-Stop Tour of 4 New CSS Color Features https://css-tricks.com/new-css-color-features-preview/ https://css-tricks.com/new-css-color-features-preview/#comments Thu, 10 Feb 2022 23:01:55 +0000 https://css-tricks.com/?p=362285 I was just writing in my “What’s new in since CSS3?” article about recent and possible future changes to CSS colors. It’s weirdly a lot. There are just as many or more new and upcoming ways to define colors than …


A Whistle-Stop Tour of 4 New CSS Color Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I was just writing in my “What’s new in since CSS3?” article about recent and possible future changes to CSS colors. It’s weirdly a lot. There are just as many or more new and upcoming ways to define colors than what we have now. I thought we’d take a really quick look.

First, a major heads up. This stuff is so complicated. I barely understand it. But here are some aspects:

  • Before all this upcoming change, we only had RGB as a color model, and everything dealt with that.
  • We had different “color spaces” that handled it differently (e.g. the rgb() function mapped that RGB color model as a cube with linear coordinates, the hsl() function mapped that RGB color model as a cylinder) but it was all sRGB gamut.
  • With the upcoming changes, we’re getting new color models and (!) we’re getting new functions that map that color model differently. So I think it’s kind of a double-triple whammy.

I can’t personally educate you on all the nitty-gritty details — I’m writing this because I bet there are a lot of you like me, wondering why you should care at all about this, and this is my attempt to understand why I should care about all of it.

Display-P3 is one that opens up a ton of more vibrant color that was able to be expressed before.

body {
  background: color(display-p3 1 0.08 0); /* super red! */
}

It turns out that modern monitors can display way more colors, particularly extra vibrant ones, but we just have no way of defining those colors with classic CSS color syntaxes, like HEX, RGB, and HSL. Super weird, right?! But if you use Display-P3, you get a wider range of access to these vibrant colors.

Screenshot of a super bright pink in a CodePen preview using the display-p3 CSS color syntax.
That white line in Safari DevTools is showing us the “extra” range of Display-P3

The dev shop Panic latched onto this early on and started using these colors as a “secret weapon”:

Jen Simmons also covers how to use them, including a fallback for non-supporting browsers:

Resources

HWB is the one that is more “for humans” except that’s a bit debatable and it’s still based on sRGB.

I had no idea hwb() was a thing — shout out to Stefan Judis for blogging about it.

I normally think of HSL as the CSS color format that is “for humans” (and good for programmatic control) because, well, manipulating 360° of Hue and 0-100% of both Saturation and Lightness make some kind of obvious sense.

But in hwb(), we’ve got Hue (the same as HSL, I think), then Whiteness and Blackness. Stefan:

Adding White and Black to a color affects its saturation. Suppose you add the same amount of White and Black to a color, the color tone stays the same, but color loses saturation. This works up to 50% White and 50% Black (hwb(0deg 50% 50%)), which results in an achromatic color.

Showing six gradients going from red to black and the impact that change CSS color values in hwb has on the transition between colors.

Stefan expressed some doubt that this is any easier to understand than HSL, and I tend to agree. I probably just need to get more used to it, but it seems to be more abstract than simply changing the lightness or saturation.

HWB is limited to the same color gamut (sRGB) as all the old color formats all. No new colors are unlocked here.

Resources

LAB is like rgb() of a much wider gamut

div {
  background: lab(150% -400 400);
}

I liked Eric Portis’ explanation of LAB when I went around asking about it:

LAB is like RGB in that there are three linear components. Lower numbers mean less of the thing, bigger numbers mean more of the thing. So you could use LAB to specify the brightest, greenest green that ever bright-greened, and it’ll be super bright and green for everybody, but brighter and greener on monitors with wider gamuts.

So, we get all the extra color, which is awesome, but sRGB had this other problem (aside from being limited in color expression), that it isn’t perceptually uniform. Brian Kardell:

The sRGB space is not perceptually uniform. The same mathematical movement has different degrees of perceived effect depending on where you are at in the color space. If you want to read a designer’s experience with this, here’s an interesting example which does a good job struggling to do well.

The classic example here is how, in HSL, colors with the exact same “Lightness” really don’t feel the same at all.

But in LAB, apparently, it is perceptually uniform, meaning that programmatically manipulating colors is a much more sane task. And another bonus is that LAB colors are specced as being device-independent. Here’s Michelle Barker:

LAB and LCH are defined in the specification as device-independent colors. LAB is a color space that can be accessed in software like Photoshop and is recommended if you want a color to look the same on-screen as, say, printed on a t-shirt.

Resources

LCH is like hsl() of a much wider gamut

Remember how I said HSL is “for humans” in that it is easier understand than RGB? Changing the Hue, Saturation, and Lightness makes a lot of logical sense. Similar here with lch() where we’ve got Lightness, Chroma, and Hue. Back to my conversation with Eric Portis:

LCH is more like HSL: a polar space. H = hue = a circle. So doing math to pick complementary colors (or whatever transforms you’re after) becomes trivial (just add 180 — or whatever!)

I suppose you’d pick LCH just because you like the syntax of it or because it makes some complicated programmatic thing you’re trying to do easier — and you get the fact that it can express 50% more colors for free.

We get the perceptual uniformity here, too. Here’s Lea Verou who seems excited that lightness will actually mean something:

In HSL, lightness is meaningless. Colors can have the same lightness value, with wildly different perceptual lightness. […] With LCH, any colors with the same lightness are equally perceptually light, and any colors with the same chroma are equally perceptually saturated.

Another benefit of the new model is that we can wipe our hands clean of the “gray dead zone” in CSS color gradients. I think because of this perceptual uniformity stuff, two rich colors won’t get cheeky and gradient themselves through non-rich territory.

Two gradients going from blue to pink, one on top of the other. The first uses the LCH CSS color syntax and the second use HSL. HSL has noticeable gray areas.
There will always be tradeoffs in color models, especially with gradients. (Demo)

Here’s a small personal prediction: I’d say that lch() is probably going to be a designer favorite. Soon there are going to be a ton of new color choices and it’s too difficult and weird to always be picking different ones. LCH seems to have the most bang for the mental buck.

Resources

“OK”

LAB ‘n’ friends seems so new because it is new… to CSS. But LAB was invented in the 1940s. In a conversation with Adam Argyle, he used a memorable phrase: All the color spaces have an Achilles’ heel. That is, something they kinda suck at. For sRGB, it’s the grey dead zone thing, as well as the limited color gamut. LAB is great and all, but it certainly has its own weaknesses. For example, a blue-to-white gradient in LAB travels pretty awkwardly through purpletown.

In December 2020, Björn Ottosson is all like “Hey, a new color space just dropped,” and now OKLAB exists. Apparently the CSS powers-that-be see enough value in that color space that both oklab() and oklch() are already specced. I guess we should care because they are just generally better, but don’t quote me on that.

Why is it Display P3 uses the color() function but the other’s don’t?

I don’t really know. I think the CSS color() function is a bit newer and that’s just how Safari dunked it in there to start. I have no idea if Display P3 will get its own dedicated function, or if we all should just start using CSS color(), or what.

/* This is how you use Display P3 */
color(display-p3 1 0.08 0); 

/* But this doesn't work */
color(oklch 42.1% 0.192 328.6);

/* You gotta do this instead 🤷‍♀️ */
oklch(42.1% 0.192 328.6);

/* But you can use the color space within a gradient... */
background-image: linear-gradient(
    to right 
    in oklch,
    lch(50% 100 100), 
    lch(50% 100 250)
  );

The relative color syntax is super useful.

There is this really cool ability called “relative color syntax” where you can basically deconstruct a CSS color while moving it into another format. Say you have the (obviously) most famous CSS HEX color ever, fog dog, and you wanna kick it into HSL instead:

body {
  background: hsl(from #f06d06 h s l);
}

Maybe that’s not all that useful immediately, but hey, now we’re able to add alpha to it! There is literally no other way to apply alpha to an existing HEX color, so that’s kinda huge:

body {
  background: hsl(from #f06d06 h s l / 0.5);
}

But I can also mess with it. Say I wanna saturate fog dog a bit before I add opacity because the lower opacity will naturally dull it out and I wanna combat that. I can use calc() on the implied variables there:

body {
  background: hsl(from #f06d06 h calc(s + 20%) l / 0.5);
}

That’s so cool. I’m sure we’ll see some amazing things come from this. And it certainly isn’t limited to HSL. I was just using HSL because it’s what is comfortable to me right now. I could start with the named color red and mess with it in LCH if I want:

body {
  background: lch(from red l calc(c + 15) h / 0.25);
}

This stuff is going to be most useful when liberally combined with custom properties.

There are no special functions just for alpha anymore.

Just to be clear: no commas preceding the alpha value in a CSS color function — just a forward slash instead:

/* Old! */
rgb(255, 0, 0);
rgba(255, 0, 0, 0.5);

/* New! */
rgb(255 0 0);
rgb(255 0 0 / 0.5);
hsl(0deg 40% 40%)
hsl(0deg 40% 40% / 90%) /* can be percentage rather than 0.9 or whatever */

/* The New color stuff ONLY has the single base function, no alpha secondardy function */
lab(49% 39 80)
lab(49% 39 80 / 0.25)

/* Display P3, with the color function, essentially works the same way with the slash */
color(display-p3 1 0.08 0 / 0.25); 

You can even define your own CSS color space.

But I literally can’t even think about that. It blows my mind, sorry.


A Whistle-Stop Tour of 4 New CSS Color Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/new-css-color-features-preview/feed/ 11 362285
useRainbow() https://css-tricks.com/userainbow/ https://css-tricks.com/userainbow/#comments Fri, 07 Jan 2022 14:59:04 +0000 https://css-tricks.com/?p=359823 I took a break from work and started some small, personal projects (toys). One of those small projects is potato.horse where I keep all of my doodles, visual short stories and jokes. Check it out!

However, this post is not …


useRainbow() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I took a break from work and started some small, personal projects (toys). One of those small projects is potato.horse where I keep all of my doodles, visual short stories and jokes. Check it out!

However, this post is not about my break from work, other experiments, or the site itself. People seem to like one particular technique I used in the design, notably, the background effect applied that transitions between colors when the user browses the content:

Some asked me how this effect was implemented (including going as far as reading the minified code, which is very flattering).

So, here’s a quick gist, followed up with some context:

export const useRainbowBg = () =>
useEffect(() => {
  const cb = () => {
    const viewportHeight = window.innerHeight
    const contentHeight = document.body.getBoundingClientRect().height
    const viewportsPerRotation = Math.min(
      3,
      contentHeight / viewportHeight
    )
    const from = 51
    const progress =
      window.scrollY / (viewportHeight * viewportsPerRotation)
    const h = (from + 360 * progress) % 360

    document.body.style.backgroundColor = `hsl(${h}deg, 100%, 50%)`
  }
  window.addEventListener('scroll', cb, { passive: true })
  return () => window.removeEventListener('scroll', cb)
})

In short, I map the scroll position into the hue in the HSL color notation. Let’s break this down.

Color models

There are many ways of describing colors in CSS, with the two most common ones being RGB (left) and HSL (right):

RGB is an additive color palette. This means that mixing 100% of red, green and blue produces white, mixing 100% red and 100% green but 0% blue produces yellow, and so on. This is different from, say, using oil paint or the CMYK color model, where the resulting tone would be black(-ish)1.

We’re used to this approach because it’s easy to describe in code, but specifying colors in terms of hue, saturation and luminosity seems more natural, especially if you come from a design background, or… you know, are a human being using a human language.

We’ve gotten used to RGB as developers, but in spoken language, using it would feel unnatural and confusing. Façade would be very hard to use in RGB.

On the other hand, HSL can often be much more intuitive to work with. For instance, if I want to make a color slightly colder, I can just move the hue slider a bit towards blue and I should get closer to what I have in mind. With RGB, if we make the color appear colder by including more blue, the resulting tone will be a bit brighter as the blue component contributes to the overall lightness. This means that you’d have to lower the red and green values to compensate.

To see how this works in practice, try maxing out the blue color in the example below.

The first thing that stands out is that all tones are shifted towards blue and the overall brightness of the picture is increased. In the case of the effect we’re discussing, that would be undesirable.

Now, let’s try to do the same with the HSL color circle. Drag the slider to the left, by ca. 90 degrees:

In this scenario, using HSL not only turns Susan into a vampire, but also maintains a similar2 level of brightness. And that’s exactly what I’m looking for.

So, what I mean by saying this:

How does this work? In short, I map the scroll position into the hue in the HSL color notation. Rafal, 2 days earlier

…is that that every time we detect a scroll event, I try to map it to an angle on the hue circle:

I didn’t want to start with red as it would make me hungry and the base yellow fits the design a bit better, so I applied a small initial shift—hence const from = 51 set as the initial offset.

And, as far as the basic implementation goes, that’s it!

Now, there are three other areas of improvement.

useRainbow performance

We’re triggering a repaint on every scroll, so I was a bit worried that older mobile devices, or even some hi-end laptops plugged in to 4k screens might not be able to maintain solid 60fps. But, I’m happy with the results so far. Using passive event listeners provided a bit of a boost, especially on mobile.

If I realize that performance is a problem, especially with more content down the line, I’ll probably focus on:

  • removing the unnecessary call to getBoundingClientRect on every scroll handler call, and
  • deferring or throttling background color changes using requestAnimationFrame.

I expect the first improvement to have some impact, but the benefits of the second one should be negligible.

Measure before optimizing. Obsessing about the performance only makes sense when issues become noticeable, be it through a drop in framerate or battery impact. Your iPhone Pro has more computing power than many low-end laptops, so it’s a good idea to test on those devices too. It’s good to have a crappy old Android phone exacly for that purpose if you can spare a few quid.

Perceptually uniform color spaces

You might’ve noticed that in the previous illustrations some fully saturated colors seemed darker than others. That’s because the color spaces we normally use when coding don’t reflect the way the human eye works. I’ll leave the in-depth explanation to someone much more experienced than me, but suffice to say (gross oversimplification alert!) that, generally, the same amount of red/green/yellow will appear brighter than blue. This means that in some cases the text on the page will be harder to read.

For now, this isn’t an issue as I’ve just put this thing online and titles serve a secondary purpose. But there’s a solution to the problem and it’s not overly complicated: use a perceptually uniform color space. There’s a bunch of libraries that do it out of the box, both in JavaScript/TypeScript and CSS/Sass/<pick your CSS flavor here>. hsluv seems like a good starting point.

Accessiblity

Note that I’ll be focusing on the visual effect itself and not discussing the rest of the site (e.g. alt tags, document structure, etc…). I’d like to focus on contrast, color blindness and people who rely on prefers-reduced-motion. The site is a living document; there’s always so much to improve. For instance, contrast can be an issue in a few, non-critical places. I’m happy to accept feedback and implement it: hit me up!.

color blindness

I wanted to make sure that the effect doesn’t break the site completely for people with color blindness. So I focused on the most common types: deuteranomaly and protanomaly (red-green color blindness), but also ran wider tests. I used Photoshop and Colorblindly (Chrome extension) for some rudimentary checks.

prefers-reduced-motion

The prefers-reduced-motion CSS media feature is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses.

MDN

This site doesn’t contain many animations (besides the Little Sausage Angels you’ll see if you hit “Share”), but I was wondering if people who rely on prefers-reduced-motion would like the background color to stay constant.

The short answer is: I don’t know. My intuition is that rotating colors don’t really qualify as motion, but my experience and understanding of the problem is, to say the least, limited. In situations like this, I’d rather depend on user research than guesses.

Luckily, the site had its five minutes of fame on Reddit which proved to be a decent opportunity to collect feedback. None of the users brought up an issue with the background effect so far. I’m also lucky enough to know a bunch of accessibility specialists, such as Sandrina Pereira. Her suggestion was that (a) background animations definitely qualify as motion, and (b) perhaps the effect feels natural because it’s a direct result of a user interaction.

Summary

The late-90s Geocities web felt playful and weird. It was fun in an uninhibited, somewhat less performative, way. I wanted to incorporate some of this look and feel in the site. But still, I didn’t want to make it feel esoteric to the point where you’d need to up your hipsterdom-level to 9000 and browse it exclusively throught Netscape 7. All of that, while listening to the new Nirvana Unplugged album.

I still wanted decent UX on mobile and desktop, and some space for easter eggs (something you can’t do when living in the strange and abusive relationship with social media we’ve grown so accustomed to).

As a kid, I had built six websites before I even got access to the Internet for the first time. Now, after being burned out for three years, even considering changing my job, it was the first time I genuinely enjoyed coding. I forgot how much fun it was!

Now, go out, pet your cat, and make stuff!


P.S. Check out Cameron’s World.

P.P.S. The code for interactive diagrams can be found on GitHub.

Footnotes

  1. Hence the K component in CMYK meaning “black.” Using B would be confusing as it means “blue” in other color models.
  2. It’s not perfect since the perceptual color space differs from what’s described using RGB/HSL.

useRainbow() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/userainbow/feed/ 3 359823
Color Alpha Anywhere https://css-tricks.com/color-alpha-anywhere/ https://css-tricks.com/color-alpha-anywhere/#comments Tue, 16 Nov 2021 01:03:32 +0000 https://css-tricks.com/?p=356611 In my “Different Degrees of Custom Property Usage” article, I noted a situation about colors and CSS custom properties where I went “too far” with breaking up HSL color values. Breaking every single color into its H, S, and L parts …


Color Alpha Anywhere originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In my “Different Degrees of Custom Property Usage” article, I noted a situation about colors and CSS custom properties where I went “too far” with breaking up HSL color values. Breaking every single color into its H, S, and L parts is may be a step too far.

But you probably do want to split it up like this:

html {
  --color-1-hsl: 200deg 15% 73%;
  --color-1: hsl(var(--color-1-hsl));
}

So, two custom properties per color in your color system. Why? Because now you’ve got a really easy way to use it and you’ve got a way to apply alpha to the color if you want.

.card {
  background: var(--color-1);
}
.card-with-alpha {
  background: hsl(var(--color-1-hsl) / 0.5);
}

There’s not really any other way to take an existing color in CSS and apply alpha transparency to it. Well, I say that, but actually

/* Future CSS! (works in Safari TP right now) */
.card-with-alpha {
  background: hsl(from var(--color-1) h s l / 0.5);
}

That’s neat, but I’m not entirely sure when we’ll be able to rely on that in production.

You know what else we can’t use for anything super important in production? Houdini paint worklets. No Firefox or Safari yet on those.

A bummer, because Dave almost had this thing cracked! The insight here is that Houdini paint worklets basically return an image that you paint with <canvas> APIs. You can paint a rectangle in Canvas with any color format, then set the globalAlpha, return that as an image, and so that basically unlocks alpha on any color format! It works (in Chrome):

Dave chucked the code on GitHub and blogged it. Of course, it made a good video as well:

Like and subscribe.

But if you need a system like this on production, just do the custom properties technique I listed first.

A previous version of this post was tweetbombed, but I’m blogging it here because real bloggers blog.


Color Alpha Anywhere originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/color-alpha-anywhere/feed/ 5 356611
Icon Glassmorphism Effect in CSS https://css-tricks.com/icon-glassmorphism-effect-in-css/ https://css-tricks.com/icon-glassmorphism-effect-in-css/#comments Mon, 08 Nov 2021 14:57:42 +0000 https://css-tricks.com/?p=322098 I recently came across a cool effect known as glassmorphism in a Dribble shot. My first thought was I could quickly recreate it in a few minutes if I just use some emojis for the icons without wasting time …


Icon Glassmorphism Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently came across a cool effect known as glassmorphism in a Dribble shot. My first thought was I could quickly recreate it in a few minutes if I just use some emojis for the icons without wasting time on SVG-ing them.

Animated gif. Shows a nav bar with four grey icons. On :hover/ :focus, a tinted icon slides and rotates, partly coming out from behind the grey one. In the area where they overlap, we have a glassmorphism effect, with the icon in the back seen as blurred through the semitransparent grey one in front.
The effect we’re after.

I couldn’t have been more wrong about those “few minutes” — they ended up being days of furiously and frustratingly scratching this itch!

It turns out that, while there are resources on how to CSS such an effect, they all assume the very simple case where the overlay is rectangular or at most a rectangle with border-radius. However, getting a glassmorphism effect for irregular shapes like icons, whether these icons are emojis or proper SVGs, is a lot more complicated than I expected, so I thought it would be worth sharing the process, the traps I fell into and the things I learned along the way. And also the things I still don’t understand.

Why emojis?

Short answer: because SVG takes too much time. Long answer: because I lack the artistic sense of just drawing them in an image editor, but I’m familiar with the syntax enough such that I can often compact ready-made SVGs I find online to less than 10% of their original size. So, I cannot just use them as I find them on the internet — I have to redo the code to make it super clean and compact. And this takes time. A lot of time because it’s detail work.

And if all I want is to quickly code a menu concept with icons, I resort to using emojis, applying a filter on them in order to make them match the theme and that’s it! It’s what I did for this liquid tab bar interaction demo — those icons are all emojis! The smooth valley effect makes use of the mask compositing technique.

Animated gif. Shows a white liquid navigation bar with five items, one of which is selected. The selected one has a smooth valley at the top, with a dot levitating above it. It's also black, while the non-selected ones are grey in the normal state and beige in the :hover/ :focus state. On clicking another icon, the selection smoothly changes as the valley an the levitating dot slide to always be above the currently selected item.
Liquid navigation.

Alright, so this is going to be our starting point: using emojis for the icons.

The initial idea

My first thought was to stack the two pseudos (with emoji content) of the navigation links, slightly offset and rotate the bottom one with a transform so that they only partly overlap. Then, I’d make the top one semitransparent with an opacity value smaller than 1, set backdrop-filter: blur() on it, and that should be just about enough.

Now, having read the intro, you’ve probably figured out that didn’t go as planned, but let’s see what it looks like in code and what issues there are with it.

We generate the nav bar with the following Pug:

- let data = {
-   home: { ico: '🏠', hue: 200 }, 
-   notes: { ico: '🗒️', hue: 260 }, 
-   activity: { ico: '🔔', hue: 320 }, 
-   discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;

nav
  - for(let i = 0; i > n; i++)
    a(href='#' data-ico=e[i][1].ico style=`--hue: ${e[i][1].hue}deg`) #{e[i][0]}

Which compiles to the HTML below:

<nav>
  <a href='#' data-ico='🏠' style='--hue: 200deg'>home</a>
  <a href='#' data-ico='🗒️' style='--hue: 260deg'>notes</a>
  <a href='#' data-ico='🔔' style='--hue: 320deg'>activity</a>
  <a href='#' data-ico='🧭' style='--hue: 30deg'>iscovery</a>
</nav>

We start with layout, making our elements grid items. We place the nav in the middle, give links explicit widths, put both pseudos for each link in the top cell (which pushes the link text content to the bottom cell) and middle-align the link text and pseudos.

body, nav, a { display: grid; }

body {
  margin: 0;
  height: 100vh;
}

nav {
  grid-auto-flow: column;
  place-self: center;
  padding: .75em 0 .375em;
}

a {
  width: 5em;
  text-align: center;
  
  &::before, &::after {
    grid-area: 1/ 1;
    content: attr(data-ico);
  }
}
Screenshot. Shows the four menu items lined up in a row in the middle of the page, each item occupying a column, all columns having the same width; with emojis above the link text, both middle-aligned horizontally.
Firefox screenshot of the result after we got layout basics sorted.

Note that the look of the emojis is going to be different depending on the browser you’re using view the demos.

We pick a legible font, bump up its size, make the icons even bigger, set backgrounds, and a nicer color for each of the links (based on the --hue custom property in the style attribute of each):

body {
  /* same as before */
  background: #333;
}

nav {
  /* same as before */
  background: #fff;
  font: clamp(.625em, 5vw, 1.25em)/ 1.25 ubuntu, sans-serif;
}

a {
  /* same as before */
  color: hsl(var(--hue), 100%, 50%);
  text-decoration: none;
  
  &::before, &::after {
    /* same as before */
    font-size: 2.5em;
  }
}
Screenshot. Shows the same layout as before, only with a prettier and bigger font and even bigger icons, backgrounds and each menu item having a different color value based on its --hue.
Chrome screenshot of the result (live demo) after prettifying things a bit.

Here’s where things start to get interesting because we start differentiating between the two emoji layers created with the link pseudos. We slightly move and rotate the ::before pseudo, make it monochrome with a sepia(1) filter, get it to the right hue, and bump up its contrast() — an oldie but goldie technique from Lea Verou. We also apply a filter: grayscale(1) on the ::after pseudo and make it semitransparent because, otherwise, we wouldn’t be able to see the other pseudo through it.

a {
  /* same as before */
  
  &::before {
    transform: 
      translate(.375em, -.25em) 
      rotate(22.5deg);
    filter: 
      sepia(1) 
      hue-rotate(calc(var(--hue) - 50deg)) 
      saturate(3);
  }
	
  &::after {
    opacity: .5;
    filter: grayscale(1);
  }
}
Screenshot. Same nav bar as before, only now the top icon layer is grey and semitransparent, while the bottom one is slightly offset and rotated, mono in the specified --hue.
Chrome screenshot of the result (live demo) after differentiating between the two icon layers.

Hitting a wall

So far, so good… so what? The next step, which I foolishly thought would be the last when I got the idea to code this, involves setting a backdrop-filter: blur(5px) on the top (::after) layer.

Note that Firefox still needs the gfx.webrender.all and layout.css.backdrop-filter.enabled flags set to true in about:config in order for the backdrop-filter property to work.

Animated gif. Shows how to find the flags mentioned above (gfx.webrender.all and layout.css.backdrop-filter.enabled) in order to ensure they are set to true. Go to about:config, start typing their name in the search box and double click their value to change it if it's not set to true already.
The flags that are still required in Firefox for backdrop-filter to work.

Sadly, the result looks nothing like what I expected. We get a sort of overlay the size of the entire top icon bounding box, but the bottom icon isn’t really blurred.

Screenshot collage. Shows the not really blurred, but awkward result with an overlay the size of the top emoji box after applying the backdrop-filter property. This happens both in Chrome (top) and in Firefox (bottom).
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) after applying backdrop-filter.

However, I’m pretty sure I’ve played with backdrop-filter: blur() before and it worked, so what the hairy heck is going on here?

Screenshot. Shows a working glassmorphism effect, created via a control panel where we draw some sliders to get the value for each filter function.
Working glassmorphism effect (live demo) in an older demo I coded.

Getting to the root of the problem

Well, when you have no idea whatsoever why something doesn’t work, all you can do is take another working example, start adapting it to try to get the result you want… and see where it breaks!

So let’s see a simplified version of my older working demo. The HTML is just an article in a section. In the CSS, we first set some dimensions, then we set an image background on the section, and a semitransparent one on the article. Finally, we set the backdrop-filter property on the article.

section { background: url(cake.jpg) 50%/ cover; }

article {
  margin: 25vmin;
  height: 40vh;
  background: hsla(0, 0%, 97%, .25);
  backdrop-filter: blur(5px);
}
Screenshot. Shows a working glassmorphism effect, where we have a semitransparent box on top of its parent one, having an image background.
Working glassmorphism effect (live demo) in a simplified test.

This works, but we don’t want our two layers nested in one another; we want them to be siblings. So, let’s make both layers article siblings, make them partly overlap and see if our glassmorphism effect still works.

<article class='base'></article>
<article class='grey'></article>
article { width: 66%; height: 40vh; }

.base { background: url(cake.jpg) 50%/ cover; }

.grey {
  margin: -50% 0 0 33%;
  background: hsla(0, 0%, 97%, .25);
  backdrop-filter: blur(5px);
}
Screenshot collage. Shows the case where we have a semitransparent box on top of its sibling having an image background. The top panel screenshot was taken in Chrome, where the glassmorphism effect works as expected. The bottom panel screenshot was taken in Firefox, where things are mostly fine, but the blur handling around the edges is really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the two layers are siblings.

Everything still seems fine in Chrome and, for the most part, Firefox too. It’s just that the way blur() is handled around the edges in Firefox looks awkward and not what we want. And, based on the few images in the spec, I believe the Firefox result is also incorrect?

I suppose one fix for the Firefox problem in the case where our two layers sit on a solid background (white in this particular case) is to give the bottom layer (.base) a box-shadow with no offsets, no blur, and a spread radius that’s twice the blur radius we use for the backdrop-filter applied on the top layer (.grey). Sure enough, this fix seems to work in our particular case.

Things get a lot hairier if our two layers sit on an element with an image background that’s not fixed (in which case, we could use a layered backgrounds approach to solve the Firefox issue), but that’s not the case here, so we won’t get into it.

Still, let’s move on to the next step. We don’t want our two layers to be two square boxes, we want then to be emojis, which means we cannot ensure semitransparency for the top one using a hsla() background — we need to use opacity.

.grey {
  /* same as before */
  opacity: .25;
  background: hsl(0, 0%, 97%);
}
Screenshot. Shows the case where we have a subunitary opacity on the top layer in order to make it semitransparent, instead of a subunitary alpha value for the semitransparent background.
The result (live demo) when the top layer is made semitransparent using opacity instead of a hsla() background.

It looks like we found the problem! For some reason, making the top layer semitransparent using opacity breaks the backdrop-filter effect in both Chrome and Firefox. Is that a bug? Is that what’s supposed to happen?

Bug or not?

MDN says the following in the very first paragraph on the backdrop-filter page:

Because it applies to everything behind the element, to see the effect you must make the element or its background at least partially transparent.

Unless I don’t understand the above sentence, this appears to suggest that opacity shouldn’t break the effect, even though it does in both Chrome and Firefox.

What about the spec? Well, the spec is a huge wall of text without many illustrations or interactive demos, written in a language that makes reading it about as appealing as sniffing a skunk’s scent glands. It contains this part, which I have a feeling might be relevant, but I’m unsure that I understand what it’s trying to say — that the opacity set on the top element that we also have the backdrop-filter on also gets applied on the sibling underneath it? If that’s the intended result, it surely isn’t happening in practice.

The effect of the backdrop-filter will not be visible unless some portion of element B is semi-transparent. Also note that any opacity applied to element B will be applied to the filtered backdrop image as well.

Trying random things

Whatever the spec may be saying, the fact remains: making the top layer semitransparent with the opacity property breaks the glassmorphism effect in both Chrome and Firefox. Is there any other way to make an emoji semitransparent? Well, we could try filter: opacity()!

At this point, I should probably be reporting whether this alternative works or not, but the reality is… I have no idea! I spent a couple of days around this step and got to check the test countless times in the meanwhile — sometimes it works, sometimes it doesn’t in the exact same browsers, wit different results depending on the time of day. I also asked on Twitter and got mixed answers. Just one of those moments when you can’t help but wonder whether some Halloween ghost isn’t haunting, scaring and scarring your code. For eternity!

It looks like all hope is gone, but let’s try just one more thing: replacing the rectangles with text, the top one being semitransparent with color: hsla(). We may be unable to get the cool emoji glassmorphism effect we were after, but maybe we can get such a result for plain text.

So we add text content to our article elements, drop their explicit sizing, bump up their font-size, adjust the margin that gives us partial overlap and, most importantly, replace the background declarations in the last working version with color ones. For accessibility reasons, we also set aria-hidden='true' on the bottom one.

<article class='base' aria-hidden='true'>Lion 🧡</article>
<article class='grey'>Lion 🖤</article>
article { font: 900 21vw/ 1 cursive; }

.base { color: #ff7a18; }

.grey {
  margin: -.75em 0 0 .5em;
  color: hsla(0, 0%, 50%, .25);
  backdrop-filter: blur(5px);
}
Screenshot collage. Shows the case where we have a semitransparent text layer on top of its identical solid orange text sibling. The top panel screenshot was taken in Chrome, where we get proper blurring, but it's underneath the entire bounding box of the semitransparent top text, not limited to just the actual text. The bottom panel screenshot was taken in Firefox, where things are even worse, with the blur handling around the edges being really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when we have two text layers.

There are couple of things to note here.

First, setting the color property to a value with a subunitary alpha also makes emojis semitransparent, not just plain text, both in Chrome and in Firefox! This is something I never knew before and I find absolutely mindblowing, given the other channels don’t influence emojis in any way.

Second, both Chrome and Firefox are blurring the entire area of the orange text and emoji that’s found underneath the bounding box of the top semitransparent grey layer, instead of just blurring what’s underneath the actual text. In Firefox, things look even worse due to that awkward sharp edge effect.

Even though the box blur is not what we want, I can’t help but think it does make sense since the spec does say the following:

[…] to create a “transparent” element that allows the full filtered backdrop image to be seen, you can use “background-color: transparent;”.

So let’s make a test to check what happens when the top layer is another non-rectangular shape that’s not text, but instead obtained with a background gradient, a clip-path or a mask!

Screenshot collage. Shows the case where we have semitransparent non-rectangular shaped layers (obtained with three various methods: gradient background, clip-path and mask) on top of a rectangular siblings. The top panel screenshot was taken in Chrome, where things seem to work fine in the clip-path and mask case, but not in the gradient background case. In this case, everything that's underneath the bounding box of the top element gets blurred, not just what's underneath the visible part. The bottom panel screenshot was taken in Firefox, where, regardless of the way we got the shape, everything underneath its bounding box gets blurred, not just what's underneath the actual shape. Furthermore, in all three cases we have the old awkward sharp edge issue we've had in Firefox before
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the top layer is a non-rectangular shape.

In both Chrome and Firefox, the area underneath the entire box of the top layer gets blurred when the shape is obtained with background: gradient() which, as mentioned in the text case before, makes sense per the spec. However, Chrome respects the clip-path and mask shapes, while Firefox doesn’t. And, in this case, I really don’t know which is correct, though the Chrome result does make more sense to me.

Moving towards a Chrome solution

This result and a Twitter suggestion I got when I asked how to make the blur respect the text edges and not those of its bounding box led me to the next step for Chrome: applying a mask clipped to the text on the top layer (.grey). This solution doesn’t work in Firefox for two reasons: one, text is sadly a non-standard mask-clip value that only works in WebKit browsers and, two, as shown by the test above, masking doesn’t restrict the blur area to the shape created by the mask in Firefox anyway.

/* same as before */

.grey {
  /* same as before */
  -webkit-mask: linear-gradient(red, red) text; /* only works in WebKit browsers */
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one).
Chrome screenshot of the result (live demo) when the top layer has a mask restricted to the text area.

Alright, this actually looks like what we want, so we can say we’re heading in the right direction! However, here we’ve used an orange heart emoji for the bottom layer and a black heart emoji for the top semitransparent layer. Other generic emojis don’t have black and white versions, so my next idea was to initially make the two layers identical, then make the top one semitransparent and use filter: grayscale(1) on it.

article { 
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 cursive;
}

.grey {
  --a: .25;
  margin: -1em 0 0 .5em;
  filter: grayscale(1);
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red, red) text;
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one). The problem is that applying the grayscale filter on the top semitransparent layer not only affects this layer, but also the blurred area of the layer underneath.
Chrome screenshot of the result (live demo) when the top layer gets a grayscale(1) filter.

Well, that certainly had the effect we wanted on the top layer. Unfortunately, for some weird reason, it seems to have also affected the blurred area of the layer underneath. This moment is where to briefly consider throwing the laptop out the window… before getting the idea of adding yet another layer.

It would go like this: we have the base layer, just like we have so far, slightly offset from the other two above it. The middle layer is a “ghost” (transparent) one that has the backdrop-filter applied. And finally, the top one is semitransparent and gets the grayscale(1) filter.

body { display: grid; }

article {
  grid-area: 1/ 1;
  place-self: center;
  padding: .25em;
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 pacifico, z003, segoe script, comic sans ms, cursive;
}

.base { margin: -.5em 0 0 -.5em; }

.midl {
  --a: 0;
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red, red) text;
}

.grey { filter: grayscale(1) opacity(.25) }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent grey, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on a middle, completely transparent one).
Chrome screenshot of the result (live demo) with three layers.

Now we’re getting somewhere! There’s just one more thing left to do: make the base layer monochrome!

/* same as before */

.base {
  margin: -.5em 0 0 -.5em;
  filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Chrome screenshot of the result (live demo) we were after.

Alright, this is the effect we want!

Getting to a Firefox solution

While coding the Chrome solution, I couldn’t help but think we may be able to pull off the same result in Firefox since Firefox is the only browser that supports the element() function. This function allows us to take an element and use it as a background for another element.

The idea is that the .base and .grey layers will have the same styles as in the Chrome version, while the middle layer will have a background that’s (via the element() function) a blurred version of our layers.

To make things easier, we start with just this blurred version and the middle layer.

<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>

We absolutely position the blurred version (still keeping it in sight for now), make it monochrome and blur it and then use it as a background for .midl.

#blur {
  position: absolute;
  top: 2em; right: 0;
  margin: -.5em 0 0 -.5em;
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.midl {
  --a: .5;
  background: -moz-element(#blur);
}

We’ve also made the text on the .midl element semitransparent so we can see the background through it. We’ll make it fully transparent eventually, but for now, we still want to see its position relative to the background.

Firefox screenshot. Shows a blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the blurred element via the element() function.
Firefox screenshot of the result (live demo) when using the blurred element #blur as a background.

We can notice a one issue right away: while margin works to offset the actual #blur element, it does nothing for shifting its position as a background. In order to get such an effect, we need to use the transform property. This can also help us if we want a rotation or any other transform — as it can be seen below where we’ve replaced the margin with transform: rotate(-9deg).

Firefox screenshot. Shows a slightly rotated blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly rotated blurred element via the element() function.
Firefox screenshot of the result (live demo) when using transform: rotate() instead of margin on the #blur element.

Alright, but we’re still sticking to just a translation for now:

#blur {
  /* same as before */
  transform: translate(-.25em, -.25em); /* replaced margin */
}
Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore.
Firefox screenshot of the result (live demo) when using transform: translate() instead of margin on the #blur element.

One thing to note here is that a bit of the blurred background gets cut off as it goes outside the limits of the middle layer’s padding-box. That doesn’t matter at this step anyway since our next move is to clip the background to the text area, but it’s good to just have that space since the .base layer is going to get translated just as far.

Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. It also means that the translated background text may not fully be within the limits of the padding-box anymore, as highlighted in this screenshot, which also shows the element boxes overlays.
Firefox screenshot highlighting how the translated #blur background exceeds the limits of the padding-box on the .midl element.

So, we’re going to bump up the padding by a little bit, even if, at this point, it makes absolutely no difference visually as we’re also setting background-clip: text on our .midl element.

article {
  /* same as before */
  padding: .5em;
}

#blur {
  position: absolute;
  bottom: 100vh;
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.midl {
  --a: .1;
  background: -moz-element(#blur);
  background-clip: text;
}

We’ve also moved the #blur element out of sight and further reduced the alpha of the .midl element’s color, as we want a better view at the background through the text. We’re not making it fully transparent, but still keeping it visible for now just so we know what area it covers.

Firefox screenshot. Shows a text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to a blurred element (now positioned out of sight) via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. We have also clipped the background of this element to the text, so that none of the background outside it is visible. Even so, there's enough padding room so that the blurred background is contained within the padding-box.
Firefox screenshot of the result (live demo) after clipping the .midl element’s background to text.

The next step is to add the .base element with pretty much the same styles as it had in the Chrome case, only replacing the margin with a transform.

<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
  position: absolute;
  bottom: 100vh;
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}

Since a part of these styles are common, we can also add the .base class on our blurred element #blur in order to avoid duplication and reduce the amount of code we write.

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
  --r: 5px;
  position: absolute;
  bottom: 100vh;
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. In spite of DOM order, the .base layer still shows up on top.
Firefox screenshot of the result (live demo) after adding the .base layer.

We have a different problem here. Since the .base layer has a transform, it’s now on top of the .midl layer in spite of DOM order. The simplest fix? Add z-index: 2 on the .midl element!

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer, it now shows up on top of the .base one.
Firefox screenshot of the result (live demo) after fixing the layer order such that .base is underneath .midl.

We still have another, slightly more subtle problem: the .base element is still visible underneath the semitransparent parts of the blurred background we’ve set on the .midl element. We don’t want to see the sharp edges of the .base layer text underneath, but we are because blurring causes pixels close to the edge to become semitransparent.

Screenshot. Shows two lines of blue text with a red outline to highlight the boundaries of the actual text. The text on the second line is blurred and it can be seen how this causes us to have semitransparent blue pixels on both sides of the red outline - both outside and inside.
The blur effect around the edges.

Depending on what kind of background we have on the parent of our text layers, this is a problem that can be solved with a little or a lot of effort.

If we only have a solid background, the problem gets solved by setting the background-color on our .midl element to that same value. Fortunately, this happens to be our case, so we won’t go into discussing the other scenario. Maybe in another article.

.midl {
  /* same as before */
  background: -moz-element(#blur) #fff;
  background-clip: text;
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer and having set a fully opaque background-color on it, the .base layer now lies underneath it and it isn't visible through any semitransparent parts in the text area because there aren't any more such parts.
Firefox screenshot of the result (live demo) after ensuring the .base layer isn’t visible through the background of the .midl one.

We’re getting close to a nice result in Firefox! All that’s left to do is add the top .grey layer with the exact same styles as in the Chrome version!

.grey { filter: grayscale(1) opacity(.25); }

Sadly, doing this doesn’t produce the result we want, which is something that’s really obvious if we also make the middle layer text fully transparent (by zeroing its alpha --a: 0) so that we only see its background (which uses the blurred element #blur on top of solid white) clipped to the text area:

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has transparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Since the background-color of this layer coincides to that of their parent, it is hard to see. We also have a third .grey layer, the last in DOM order. This should be right on top of the .midl one, but, due to having set a z-index on the .midl layer, the .grey layer is underneath it and not visible, in spite of the DOM order.
Firefox screenshot of the result (live demo) after adding the top .grey layer.

The problem is we cannot see the .grey layer! Due to setting z-index: 2 on it, the middle layer .midl is now above what should be the top layer (the .grey one), in spite of the DOM order. The fix? Set z-index: 3 on the .grey layer!

.grey {
  z-index: 3;
  filter: grayscale(1) opacity(.25);
}

I’m not really fond of giving out z-index layer after layer, but hey, it’s low effort and it works! We now have a nice Firefox solution:

Firefox screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Firefox screenshot of the result (live demo) we were after.

Combining our solutions into a cross-browser one

We start with the Firefox code because there’s just more of it:

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl' aria-hidden='true'>Lion 🦁</article>
<article class='grey'>Lion 🦁</article>
body { display: grid; }

article {
  grid-area: 1/ 1;
  place-self: center;
  padding: .5em;
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 cursive;
}

#blur {
  --r: 5px;
  position: absolute;
  bottom: 100vh;
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}

.midl {
  --a: 0;
  z-index: 2;
  background: -moz-element(#blur) #fff;
  background-clip: text;
}

.grey {
  z-index: 3;
  filter: grayscale(1) opacity(.25);
}

The extra z-index declarations don’t impact the result in Chrome and neither does the out-of-sight #blur element. The only things that this is missing in order for this to work in Chrome are the backdrop-filter and the mask declarations on the .midl element:

backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;

Since we don’t want the backdrop-filter to get applied in Firefox, nor do we want the background to get applied in Chrome, we use @supports:

$r: 5px;

/* same as before */

#blur {
  /* same as before */
  --r: #{$r};
}

.midl {
  --a: 0;
  z-index: 2;
  /* need to reset inside @supports so it doesn't get applied in Firefox */
  backdrop-filter: blur($r);
  /* invalid value in Firefox, not applied anyway, no need to reset */
  -webkit-mask: linear-gradient(red, red) text;
  
  @supports (background: -moz-element(#blur)) { /* for Firefox */
    background: -moz-element(#blur) #fff;
    background-clip: text;
    backdrop-filter: none;
  }
}

This gives us a cross-browser solution!

Chrome (top) and Firefox (bottom) screenshot collage of the text and emoji glassmorphism effect for comparison. The blurred backdrop seems thicker in Chrome and the emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) we were after.

While the result isn’t the same in the two browsers, it’s still pretty similar and good enough for me.

What about one-elementing our solution?

Sadly, that’s impossible.

First off, the Firefox solution requires us to have at least two elements since we use one (referenced by its id) as a background for another.

Second, while the first thought with the remaining three layers (which are the only ones we need for the Chrome solution anyway) is that one of them could be the actual element and the other two its pseudos, it’s not so simple in this particular case.

For the Chrome solution, each of the layers has at least one property that also irreversibly impacts any children and any pseudos it may have. For the .base and .grey layers, that’s the filter property. For the middle layer, that’s the mask property.

So while it’s not pretty to have all those elements, it looks like we don’t have a better solution if we want the glassmorphism effect to work on emojis too.

If we only want the glassmorphism effect on plain text — no emojis in the picture — this can be achieved with just two elements, out of which only one is needed for the Chrome solution. The other one is the #blur element, which we only need in Firefox.

<article id='blur'>Blood</article>
<article class='text' aria-hidden='true' data-text='Blood'></article>

We use the two pseudos of the .text element to create the base layer (with the ::before) and a combination of the other two layers (with the ::after). What helps us here is that, with emojis out of the picture, we don’t need filter: grayscale(1), but instead we can control the saturation component of the color value.

These two pseudos are stacked one on top of the other, with the bottom one (::before) offset by the same amount and having the same color as the #blur element. This color value depends on a flag, --f, that helps us control both the saturation and the alpha. For both the #blur element and the ::before pseudo (--f: 1), the saturation is 100% and the alpha is 1. For the ::after pseudo (--f: 0), the saturation is 0% and the alpha is .25.

$r: 5px;

%text { // used by #blur and both .text pseudos
  --f: 1;
  grid-area: 1/ 1; // stack pseudos, ignored for absolutely positioned #base
  padding: .5em;
  color: hsla(345, calc(var(--f)*100%), 55%, calc(.25 + .75*var(--f)));
  content: attr(data-text);
}

article { font: 900 21vw/ 1.25 cursive }

#blur {
  position: absolute;
  bottom: 100vh;
  filter: blur($r);
}

#blur, .text::before {
  transform: translate(-.125em, -.125em);
  @extend %text;
}

.text {
  display: grid;
	
  &::after {
    --f: 0;
    @extend %text;
    z-index: 2;
    backdrop-filter: blur($r);
    -webkit-mask: linear-gradient(red, red) text;

    @supports (background: -moz-element(#blur)) {
      background: -moz-element(#blur) #fff;
      background-clip: text;
      backdrop-filter: none;
    }
  }
}

Applying the cross-browser solution to our use case

The good news here is our particular use case where we only have the glassmorphism effect on the link icon (not on the entire link including the text) actually simplifies things a tiny little bit.

We use the following Pug to generate the structure:

- let data = {
-   home: { ico: '🏠', hue: 200 }, 
-   notes: { ico: '🗒️', hue: 260 }, 
-   activity: { ico: '🔔', hue: 320 }, 
-   discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;

nav
  - for(let i = 0; i < n; i++)
    - let ico = e[i][1].ico;
    a.item(href='#' style=`--hue: ${e[i][1].hue}deg`)
      span.icon.tint(id=`blur${i}` aria-hidden='true') #{ico}
      span.icon.tint(aria-hidden='true') #{ico}
      span.icon.midl(aria-hidden='true' style=`background-image: -moz-element(#blur${i})`) #{ico}
      span.icon.grey(aria-hidden='true') #{ico}
      | #{e[i][0]}

Which produces an HTML structure like the one below:

<nav>
  <a class='item' href='#' style='--hue: 200deg'>
    <span class='icon tint' id='blur0' aria-hidden='true'>🏠</span>
    <span class='icon tint' aria-hidden='true'>🏠</span>
    <span class='icon midl' aria-hidden='true' style='background-image: -moz-element(#blur0)'>🏠</span>
    <span class='icon grey' aria-hidden='true'>🏠</span>
    home
  </a>
  <!-- the other nav items -->
</nav>

We could probably replace a part of those spans with pseudos, but I feel it’s more consistent and easier like this, so a span sandwich it is!

One very important thing to notice is that we have a different blurred icon layer for each of the items (because each and every item has its own icon), so we set the background of the .midl element to it in the style attribute. Doing things this way allows us to avoid making any changes to the CSS file if we add or remove entries from the data object (thus changing the number of menu items).

We have almost the same layout and prettified styles we had when we first CSS-ed the nav bar. The only difference is that now we don’t have pseudos in the top cell of an item’s grid; we have the spans:

span {
  grid-area: 1/ 1; /* stack all emojis on top of one another */
  font-size: 4em; /* bump up emoji size */
}

For the emoji icon layers themselves, we also don’t need to make many changes from the cross-browser version we got a bit earlier, though there are a few lttle ones.

First off, we use the transform and filter chains we picked initially when we were using the link pseudos instead of spans. We also don’t need the color: hsla() declaration on the span layers any more since, given that we only have emojis here, it’s only the alpha channel that matters. The default, which is preserved for the .base and .grey layers, is 1. So, instead of setting a color value where only the alpha, --a, channel matters and we change that to 0 on the .midl layer, we directly set color: transparent there. We also only need to set the background-color on the .midl element in the Firefox case as we’ve already set the background-image in the style attribute. This leads to the following adaptation of the solution:

.base { /* mono emoji version */
  transform: translate(.375em, -.25em) rotate(22.5deg);
  filter: sepia(1) hue-rotate(var(--hue)) saturate(3) blur(var(--r, 0));
}

.midl { /* middle, transparent emoji version */
  color: transparent; /* so it's not visible */
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red 0 0) text;
  
  @supports (background: -moz-element(#b)) {
    background-color: #fff;
    background-clip: text;
    backdrop-filter: none;
  }
}

And that’s it — we have a nice icon glassmorphism effect for this nav bar!

Chrome (top) and Firefox (bottom) screenshot collage of the emoji glassmorphism effect for comparison. The emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the desired emoji glassmorphism effect (live demo).

There’s just one more thing to take care of — we don’t want this effect at all times; only on :hover or :focus states. So, we’re going to use a flag, --hl, which is 0 in the normal state, and 1 in the :hover or :focus state in order to control the opacity and transform values of the .base spans. This is a technique I’ve detailed in an earlier article.

$t: .3s;

a {
  /* same as before */
  --hl: 0;
  color: hsl(var(--hue), calc(var(--hl)*100%), 65%);
  transition: color $t;
  
  &:hover, &:focus { --hl: 1; }
}

.base {
  transform: 
    translate(calc(var(--hl)*.375em), calc(var(--hl)*-.25em)) 
    rotate(calc(var(--hl)*22.5deg));
  opacity: var(--hl);
  transition: transform $t, opacity $t;
}

The result can be seen in the interactive demo below when the icons are hovered or focused.

What about using SVG icons?

I naturally asked myself this question after all it took to get the CSS emoji version working. Wouldn’t the plain SVG way make more sense than a span sandwich, and wouldn’t it be simpler? Well, while it does make more sense, especially since we don’t have emojis for everything, it’s sadly not less code and it’s not any simpler either.

But we’ll get into details about that in another article!


Icon Glassmorphism Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/icon-glassmorphism-effect-in-css/feed/ 6 322098
Using JavaScript to Adjust Saturation and Brightness of RGB Colors https://css-tricks.com/using-javascript-to-adjust-saturation-and-brightness-of-rgb-colors/ https://css-tricks.com/using-javascript-to-adjust-saturation-and-brightness-of-rgb-colors/#comments Tue, 06 Oct 2020 14:36:55 +0000 https://css-tricks.com/?p=322279 Lately I’ve been taking a look into designing with color (or “colour” as we spell it where I’m from in New Zealand). Looking at Adam Wathan and Steve Schroger’s advice on the subject, we find that we’re going to …


Using JavaScript to Adjust Saturation and Brightness of RGB Colors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Lately I’ve been taking a look into designing with color (or “colour” as we spell it where I’m from in New Zealand). Looking at Adam Wathan and Steve Schroger’s advice on the subject, we find that we’re going to need more than just five nice looking hex codes from a color palette generator when building an application. We’re going to need a lot of grays and a few primary colors. From these primary colors we’ll want a variety of levels of brightness and saturation.

I’ve mainly been using hex codes or RGB colors when developing applications and I’ve found I get slowed down by trying to work out different levels of lightness and saturation from a single hue.  So, to save you from getting RSI by carefully moving the color picker in VS Code, or continually opening hexcolortool, let’s look at some code to help you manipulate those colors.

HSL values

An effective way to write web colors is to use HSL values, especially if you plan to alter the colors manually. HSL stands for hue, saturation, lightness. Using HSL, you can declare your hue as a number from 0 to 360. Then you can note down a percentage for saturation and lightness respectively. For instance:

div {
  background-color: hsl(155, 30%, 80%);
}

This will give you a light, muted, mint green color. What if we needed to throw some dark text over this div? We could use a color close to black, but consistent with the background. For example, we can grab the same HSL values and pull the lightness down to 5%: 

div {
  background-color: hsl(155, 30%, 80%);
  color: hsl(155, 30%, 5%);
}

Nice. Now we have text that is very close to black, but looks a bit more natural, and is tied to its background. But what if this wasn’t a paragraph of text, but a call-to-action button instead? We can draw some more attention by ramping up the saturation and lowering the lightness a little on the background:

.call-to-action {
  background-color: hsl(155, 80%, 60%);
  color: hsl(155, 30%, 5%);
}

Or, what if there was some text that wasn’t as important? We could turn back up the brightness on the text, and lower the saturation. This takes away some of the contrast and allows this less important text to fade into the background a bit more. That said, we need to be careful to keep a high enough contrast for accessibility and readability, so let’s lighten the background again:

div {
  background-color: hsl(155, 30%, 80%);
  color: hsl(155, 30%, 5%);
}

.lessimportant {
  color: hsl(155, 15%, 40%);
}

HSL values are supported in all major browsers and they are a superior way of defining colors compared to RGB. This is because they allow you to be more declarative with the hue, saturation and lightness of a color.

But, what if you’ve already committed to using RGB values? Or you get an email from your boss asking “is this going to work on IE 8?”

Libraries

There are a lot of great color libraries out there that are able to convert HSL values back into hex codes or RGB colors. Most of them also have a variety of manipulation functions to help build a color scheme.

Here is a list of some libraries I know:

  • If converting between formats is a problem, try colvertize by Philipp Mildenberger. It’s a lightweight library providing a lot of conversion methods and a few manipulation methods.
  • Then we have color, maintained by Josh Junon. This allows you to declare, process and extract colors using a fluent interface. It provides a variety of conversions and manipulation methods.
  • Another one is TinyColor by Brian Grinstead over at Mozilla, which can handle a whole lot of input types as well as utility functions. It also provides a few functions to help generate color schemes.

Also here is a great CSS-Tricks article on converting color formats.

Colour Grid Tool

Another option is you can try out a color tool I built called Colour Grid. To quote Refactoring UI, “As tempting as it is, you can’t rely purely on math to craft the perfect color palette.”

Naturally, after reading this, I built a React app to mathematically craft a color palette. Okay, it won’t solve all your problems, but it might start you off with some options. The app will create 100 different levels of saturation and lightness based the hue you select. You can either click a grid item to copy the hex code, or copy a color as a CSS custom property from a text area at the end. This could be something to try if you need a quick way to get variations from one or two hues. 

Here are some techniques I learned for processing RGB colors as well for if you are using RGB colors and need a way to transform them.

How to find the lightness of an RGB color

Disclaimer: This technique does not account for the intrinsic value of a hue. The intrinsic value of a hue is its inherent brightness before you’ve started adding any black or white. It’s illustrated by the fact pure yellow looks a lot brighter to us than a pure purple.

This technique produces the level of lightness based on a programmatic measure of how much white or black is mixed in. The perceived brightness is affected by more than this measure so remember to also use your eyes to judge the level of light you need.

The level of lightness of an RGB color can be worked out by finding the average of the highest and lowest of the RGB values, then dividing this by 255 (the middle color does not affect the lightness).

This will give you a decimal between zero and one representing the lightness. Here is a JavaScript function for this:

function getLightnessOfRGB(rgbString) {
  // First convert to an array of integers by removing the whitespace, taking the 3rd char to the 2nd last then splitting by ','
  const rgbIntArray = (rgbString.replace(/ /g, '').slice(4, -1).split(',').map(e => parseInt(e)));


  // Get the highest and lowest out of red green and blue
  const highest = Math.max(...rgbIntArray);
  const lowest = Math.min(...rgbIntArray);


  // Return the average divided by 255
  return (highest + lowest) / 2 / 255;
}

Here’s a CodePen using this function:

How to saturate an RGB color without changing lightness or hue

What can we do with our newfound ability to find the lightness of an RGB? It can help us saturate an RGB color without changing the lightness.

Saturating an RGB comes with a few problems, though:

  • There is no information in the RGB format of a gray color to tell us what the saturated version will look like because gray doesn’t have a hue. So if we’re going to write a function to saturate a color, we need to deal with this case.
  • We can’t actually get to a pure hue unless the color is 50% lightness — anything else will be diluted by either black or white. So we have a choice of whether to keep the same lightness as we saturate the color, or move the color towards 50% lightness to get the most vibrant version. For this example, we’ll keep the same level of lightness.

Let’s start start with the color rgb(205, 228, 219) — a light, muted cyan. To saturate a color we need to increase the difference between the lowest and highest RGB value. This will move it toward a pure hue.

If we want to keep the lightness the same, we’re going to need to increase the highest value and decrease the lowest value by an equal amount. But because the RGB values need to be clamped between 0 and 255, our saturation options will be limited when the color is lighter or darker. This means there is a range of saturation we have available for any given lightness.

Let’s grab the saturation range available for our color. We can work it out by finding the lowest of these two:

  • The difference between the RGB values of a gray with the same lightness as our color, and 255
  • The difference between the RGB values of a gray with the same lightness as our color, and 0 (which is just the gray value itself)

To get a fully gray version of a color, we can grab the end result of the getLightnessOfRGB function from the previous section and multiply it by 255. Then use this number for all three of our RGB values to get a gray that’s the same lightness as our original color. 

Let’s do this now:

// Using the previous "getLightnessOfRGB" function
const grayVal = getLightnessOfRGB('rgb(205, 228, 219)')*255; // 217
// So a gray version of our color would look like rgb(217,217,217);
// Now let's get the saturation range available:
const saturationRange =  Math.round(Math.min(255-grayVal,grayVal)); // 38

Let’s say we want to saturate the color by 50%. To do this  want to increase the highest RGB value and decrease the lowest by 50% of the saturation range. However, this may put us over 255 or under zero, so we need to clamp the change by the minimum of these two values:

  • The difference between the highest RGB value and 255
  • The difference between the lowest RGB value and 0 (which is the value itself)
// Get the maximum change by getting the minimum out of: 
// (255 - the highest value) OR (the lowest value)
const maxChange = Math.min(255-228, 205); // 27


// Now we will be changing our values by the lowest out of:
// (the saturation range * the increase fraction) OR (the maximum change)
const changeAmount = Math.min(saturationRange/0.5, maxChange) // 19

This means we need to add 19 to the highest RGB value (green) and subtract 19 from the lowest RGB value:

const redResult = 205 - 19; // 186
const greenResult= 228 + 19; // 247

What about the third value?

This is where things get a bit more complicated. The middle value’s distance from gray can be worked with the ratio between it and the distance from gray of either of the other two values.

As we move the highest and lowest values further away from gray, the middle value increases/decreases in proportion with them. 

Now let’s get the difference between the highest value and full gray. Then the difference between the middle value and the full gray. Then we’ll get the ratio between these. I’m going to also remove the rounding from working out the gray value to make this more exact:

const grayVal = getLightnessOfRGB('rgb(205, 228, 219)')*255;
const highDiff = grayVal - 228; // -11 subtracting green - the highest value
const midDiff = grayVal - 219; // -2 subtracting blue - the middle value
const middleValueRatio = midDiff / highDiff; // 0.21739130434782608

Then what we need to do is get the difference between our new RGB green value (after we added 19 to it) and the gray value, then multiply this by our ratio. We add this back on to the gray value and that’s our answer for our newly saturated blue:

// 247 is the green value after we applied the saturation transformation
const newBlue = Math.round(grayVal+(247-grayVal)*middleValueRatio); // 223

So after we’ve applied our transformations, we we get an RGB color of rgb(186, 247, 223 — a more vibrant version of the color we started with. But its kept its lightness and hue.

Here are a couple of JavaScript functions that work together to saturate a color by 10%. The second function here returns an array of objects representing the RGB values in order of size. This second function is used in all of the rest of the functions in this article.

If you give it a gray, it will just return the same color:

function saturateByTenth(rgb) {
  const rgbIntArray = (rgb.replace(/ /g, '').slice(4, -1).split(',').map(e => parseInt(e)));
  const grayVal = getLightnessOfRGB(rgb)*255;
  const [lowest,middle,highest] = getLowestMiddleHighest(rgbIntArray);


  if(lowest.val===highest.val){return rgb;}
  
  const saturationRange =  Math.round(Math.min(255-grayVal,grayVal));
  const maxChange = Math.min((255-highest.val),lowest.val);
  const changeAmount = Math.min(saturationRange/10, maxChange);
  const middleValueRatio =(grayVal-middle.val)/(grayVal-highest.val);
  
  const returnArray=[];
  returnArray[highest.index]= Math.round(highest.val+changeAmount);
  returnArray[lowest.index]= Math.round(lowest.val-changeAmount);
  returnArray[middle.index]= Math.round(grayVal+(returnArray[highest.index]-grayVal)*middleValueRatio);
   return (`rgb(${[returnArray].join()})`);
}


function getLowestMiddleHighest(rgbIntArray) {
  let highest = {val:-1,index:-1};
  let lowest = {val:Infinity,index:-1};


  rgbIntArray.map((val,index)=>{
    if(val>highest.val){
      highest = {val:val,index:index};
    }
    if(val<lowest.val){
      lowest = {val:val,index:index};
    }
  });


  if(lowest.index===highest.index){
    lowest.index=highest.index+1;
  }
  
  let middle = {index: (3 - highest.index - lowest.index)};
  middle.val = rgbIntArray[middle.index];
  return [lowest,middle,highest];
}

How to desaturate an RGB Color

If we completely desaturate a color, we’ll end up with a shade of gray. RGB grays will always have three equal RGB values, so we could just use the grayVal from the previous function to make a gray color with the same lightness as any given color.

What if we don’t want to go straight to gray, and only want to slightly desaturate a color? We can do this by reversing the previous example.

Let’s look at another example. If we start with rgb(173, 31, 104), we have a saturated rouge. Let’s grab the decimal measure of lightness and multiply it by 255 to get the gray version:

const grayVal = Math.round(getLightnessOfRGB('rgb(173, 31, 104)') * 255); // 102

This means that if we fully desaturate this color to gray we’re going to end up with rgb(102, 102, 102). Let’s desaturate it by 30%.

First, we need to find the saturation range of the color again:

const saturationRange = Math.round(Math.min(255-grayVal,grayVal)); // 102

To desaturate our color by 30%, we want to move the highest and lowest color by 30% of this range toward full gray. But we also need to clamp the change amount by the distance between either of these colors (the distance will be the same for the highest and lowest), and full gray.

// Get the maximum change by getting the difference between the lowest (green) and the gray value
const maxChange = grayVal-31; // 71
// Now grab the value that represents 30% of our saturation range
const changeAmount = Math.min(saturationRange * 0.3, maxChange) // 30.59999

And add this change amount to the lowest RGB value and subtract it from the highest value: 

const newGreen =Math.Round(31+changeAmount); // 62
const newRed =Math.Round(173-changeAmount); // 142

Then use the same ratio technique as the last function to find the value for the third color:

const highDiff = grayVal - 173; // -71 subtracting red - the highest value
const midDiff = grayVal - 104; // -2 subtracting blue - the middle value
const middleValueRatio = midDiff / highDiff; // 0.02816901408
const newBlue = Math.Round(grayVal+(142.4-grayVal)*middleValueRatio); // 103

So that means the RGB representation of our rouge desaturated by 30% would be rgb(142, 62, 103). The hue and the lightness are exactly the same, but it’s a bit less vibrant.

Here’s a JavaScript function that will desaturate a color by 10%. It’s basically a reverse of the previous function.

function desaturateByTenth(rgb) {
  const rgbIntArray = (rgb.replace(/ /g, '').slice(4, -1).split(',').map(e => parseInt(e)));
  //grab the values in order of magnitude 
  //this uses the getLowestMiddleHighest function from the saturate section
  const [lowest,middle,highest] = getLowestMiddleHighest(rgbIntArray);
  const grayVal = getLightnessOfRGB(rgb) * 255;


  if(lowest.val===highest.val){return rgb;}
  
  const saturationRange =  Math.round(Math.min(255-grayVal,grayVal));
  const maxChange = grayVal-lowest.val;
  const changeAmount = Math.min(saturationRange/10, maxChange);
                               
  const middleValueRatio =(grayVal-middle.val)/(grayVal-highest.val);
  
  const returnArray=[];
  returnArray[highest.index]= Math.round(highest.val-changeAmount);
  returnArray[lowest.index]= Math.round(lowest.val+changeAmount);
  returnArray[middle.index]= Math.round(grayVal+(returnArray[highest.index]-grayVal)*middleValueRatio);
  return (`rgb(${[returnArray].join()})`);
}



Here’s a CodePen to experiment with the effect of these saturation functions:

How to lighten an RGB color keeping the hue the same

To lighten an RGB value and keep the hue the same, we need to increase each RGB value by the same proportion of difference between the value and 255. Let’s say we have this color: rgb(0, 153, 255). That’s a fully saturated blue/cyan. Let’s look at the difference between each RGB value and 255: 

  • Red is zero, so the difference is 255. 
  • Green is 153, so the difference is 102. 
  • Blue is 255, so the difference is zero. 

Now when we lighten the color, we need to increase each RGB value by the same fraction of our differences. One thing to note is that we are essentially mixing white into our color. This means that the color will slowly lose its saturation as it lightens.

Let’s increase the lightness on this color by a tenth. We’ll start with out lowest RGB value, red. We add on a tenth of 255 to this value. We also need to use Math.min to make sure that the value doesn’t increase over 255:

const red = 0;
const newRed = Math.round( red + Math.min( 255-red, 25.5 )); // 26

Now the other two RGB values need to increase by the same fraction of distance to 255.

To work this out, we get the difference between the lowest RGB value (before we increased it) and 255. Red was zero so our difference is 255. Then we get the amount the lowest RGB value increased in our transformation. Red increased from zero to 26, so our increase is 26.

Dividing the increase by the difference between the original color and 255 gives us a fraction we can use to work out the other values.

const redDiff = 255 - red; // 255
const redIncrease = newRed - red; // 26
const increaseFraction = redIncrease / redDiff; // 0.10196

Now we multiply the difference between the other RGB values and 255 by this fraction. This gives us the amount we need to add to each value.

const newGreen = Math.round(153 + (255 - 153) * increaseFraction); // 163
const newBlue = Math.round(255 + (255 - 255) * increaseFraction); // 255

This means the color we end up with is rgb(26, 163, 255). That’s still the same hue, but a touch lighter.

Here’s a function that does this: 

function lightenByTenth(rgb) {

  const rgbIntArray = rgb.replace(/ /g, '').slice(4, -1).split(',').map(e => parseInt(e));
  // Grab the values in order of magnitude 
  // This uses the getLowestMiddleHighest function from the saturate section
  const [lowest,middle,highest]=getLowestMiddleHighest(rgbIntArray);
  
  if(lowest.val===255){
    return rgb;
  }
  
  const returnArray = [];

  // First work out increase on lower value
  returnArray[lowest.index]= Math.round(lowest.val+(Math.min(255-lowest.val,25.5)));

  // Then apply to the middle and higher values
  const increaseFraction  = (returnArray[lowest.index]-lowest.val)/ (255-lowest.val);
  returnArray[middle.index]= middle.val +(255-middle.val)*increaseFraction ;
  returnArray[highest.index]= highest.val +(255-highest.val)*increaseFraction ;
  
  // Convert the array back into an rgb string
  return (`rgb(${returnArray.join()})`);
}

How to darken an RGB color keeping the hue the same

Darkening an RGB color is pretty similar. Instead of adding to the values to get 255, we’re subtracting from the values to get toward zero.

Also we start our transformation by reducing the highest value and getting the fraction of this decrease. We use this fraction to reduce the other two values by their distance to zero. This is a reversal of what we did lightening a color.

Darkening a color will also cause it to slowly lose its level of saturation.

function darkenByTenth(rgb) {
  
  // Our rgb to int array function again
  const rgbIntArray = rgb.replace(/ /g, '').slice(4, -1).split(',').map(e => parseInt(e));
  //grab the values in order of magnitude 
  //this uses the function from the saturate function
  const [lowest,middle,highest]=getLowestMiddleHighest(rgbIntArray);
  
  if(highest.val===0){
    return rgb;
  }

  const returnArray = [];

  returnArray[highest.index] = highest.val-(Math.min(highest.val,25.5));
  const decreaseFraction  =(highest.val-returnArray[highest.index])/ (highest.val);
  returnArray[middle.index]= middle.val -middle.val*decreaseFraction; 
  returnArray[lowest.index]= lowest.val -lowest.val*decreaseFraction;              
                            
  // Convert the array back into an rgb string
  return (`rgb(${returnArray.join()}) `);
}

Here’s a CodePen to experiment with the effect of the lightness functions:


If you ever do need to work with RGB colors, these functions will help you get you started. You can also give the HSL format a try, as well as the color libraries to extend browser support, and the Colour Grid tool for conversions.


Using JavaScript to Adjust Saturation and Brightness of RGB Colors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-javascript-to-adjust-saturation-and-brightness-of-rgb-colors/feed/ 2 322279
Creating Color Themes With Custom Properties, HSL, and a Little calc() https://css-tricks.com/creating-color-themes-with-custom-properties-hsl-and-a-little-calc/ https://css-tricks.com/creating-color-themes-with-custom-properties-hsl-and-a-little-calc/#comments Thu, 16 Apr 2020 20:01:21 +0000 https://css-tricks.com/?p=306079 Before the advent of CSS custom properties (we might call them “variables” in this article as that’s the spirit of them), implementing multiple color schemes on the same website usually meant writing separate stylesheets. Definitely not the most maintainable thing …


Creating Color Themes With Custom Properties, HSL, and a Little calc() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Before the advent of CSS custom properties (we might call them “variables” in this article as that’s the spirit of them), implementing multiple color schemes on the same website usually meant writing separate stylesheets. Definitely not the most maintainable thing in the world. Nowadays, though, we can define variables in a single stylesheet and let CSS do the magic.

Even if you aren’t offering something like user-generated or user-chosen color themes, you might still use the concept of theming on your website. For example, it is fairly common to use different colors themes across different areas of the site.

We’re going to build out an example like this:

Same layout, different colors.

In this example, all that changes between sections is the color hue; the variations in lightness are always the same. Here’s an example of a simplified color palette for a specific hue:

A palette of multiple hues might look something like this:

This would take effort to do with RGB color value, but in HSL only one value changes.

Enter custom properties

Custom properties have been around for a while and are widely supported. Polyfills and other solutions for IE 11 are also available.

The syntax is very similar to traditional CSS. Here is an overview of the basic usage:

It’s common to see variables defined on the :root pseudo-element, which is always <html> in HTML, but with higher specificity. That said, variables can be defined on any element which is useful for scoping specific variables to specific elements. For example, here are variables defined on data attributes:

Adding calc() to the mix

Variables don’t have to be fixed values. We can leverage the power of the calc() function to automatically calculate values for us while adhering to a uniform pattern:

Since CSS doesn’t support loops, a preprocessor would be handy to generate a part of the code. But remember: CSS variables are not the same as Sass variables.

Implementing CSS variables

What we’re basically trying to do is change the color of the same component on different sections of the same page. Like this:

We have three sections in tabs with their own IDs: #food, #lifestyle, and #travel. Each section corresponds to a different hue. The  data-theme-attribute on the div.wrapper element defines which hue is currently in use.

When #travel is the active tab, we’re using the --first-hue variable, which has a value of 180°. That is what gets used as the --hue value on the section, resulting in a teal color:

<div class="wrapper" data-theme="travel">
.wrapper[data-theme="travel"] {
  --hue: var(--first-hue);  /* = 180° = teal */
}

Clicking any of the tabs updates the data-theme attribute to the ID of the section, while removing the hash (#) from it. This takes a smidge of JavaScript. That’s one of the (many) nice things about CSS: they can be accessed and manipulated with JavaScript. This is a far cry from preprocessor variables, which compile into values at the build stage and are no longer accessible in the DOM.

<li><a href="#food">Food</a></li>
const wrapper = document.querySelector('.wrapper');
document.querySelector("nav").addEventListener('click', e => {
  // Get theme name from URL and ditch the hash
  wrapper.dataset.theme = e.target.getAttribute('href').substr(1);
})

Progressive enhancement

When we use JavaScript, we should be mindful of scenarios where a user may have disabled it. Otherwise, our scripts — and our UI by extension — are inaccessible. This snippet ensures that the site content is still accessible, even in those situations:

// progressive enhancement:
// without JavaScript all sections are displayed, the theme is only set when the page loads
wrapper.dataset.theme = wrapper.querySelector('section').id;

This merely allows the tabs to scroll up the page to the corresponding section. Sure, theming is gone, but providing content is much more important.

While I chose to go with a single-page approach, it’s also possible to serve the sections as separate pages and set [data-theme] on the server side. 

Another approach

So far, we’ve assumed that color values change linearly and are thus subject to a mathematical approach. But even in situations where this is only partially true, we may still be able to benefit from the same concept. For instance, if lightness follows a pattern but hue doesn’t, we could split up the stylesheet like this:

<head>
  <style>
    :root {
      --hue: 260;
    }
  </style>
  <link rel="stylesheet" href="stylesheet-with-calculations-based-on-any-hue.css">
</head>

Supporting web components

Web components are an exciting (and evolving) concept. It’s enticing to think we can have encapsulated components that can be reused anywhere and theme them on a case-by-case basis. One component with many contexts!

We can use CSS variable theming with web components. It requires us to use a host-context() pseudo-selector. (Thanks to habemuscode for pointing this out to me!)

:host-context(body[data-theme="color-1"]) {
  --shade-1: var(--outsideHSL);
}

In summary…

Theming a website with CSS custom properties is much easier than the workaround approaches we’ve resorted to in the past. It’s more maintainable (one stylesheet), performant (less code), and opens up new possibilities (using JavaScript). Not to mention, CSS custom properties become even more powerful when they’re used with HSL colors and the calc() function.

We just looked at one example where we can change the color theme of a component based on the section where it is used. But again, there is much more opportunity here when we start to get into things like letting users change themes themselves – a topic that Chris explores in this article.


Creating Color Themes With Custom Properties, HSL, and a Little calc() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-color-themes-with-custom-properties-hsl-and-a-little-calc/feed/ 6 306079
The Best Color Functions in CSS? https://css-tricks.com/the-best-color-functions-in-css/ https://css-tricks.com/the-best-color-functions-in-css/#comments Mon, 20 Jan 2020 21:30:04 +0000 https://css-tricks.com/?p=301297 I’ve said before that HSL is the best color format we have. Most of us aren’t like David DeSandro, who can read hex codes. HSL(a) is Hue, Saturation, Lightness, and alpha, if we need it.

hsl(120, 100%, 40%)

Hue …


The Best Color Functions in CSS? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’ve said before that HSL is the best color format we have. Most of us aren’t like David DeSandro, who can read hex codes. HSL(a) is Hue, Saturation, Lightness, and alpha, if we need it.

hsl(120, 100%, 40%)

Hue isn’t intuitive, but it’s not that weird. You take a trip around the color wheel from 0 to 360. Saturation is more obvious where 0% has all the color sucked out, like grayscale, and 100% is fully rich color at that hue. Lightness is “normal” at 50% and adds white or black as you go toward 100% and 0%, respectively. I’m sure that’s not the correct scientific or technical way of explaining it, but that’s the brain logic.

There are still issues with using HSL, which Brian Kardell explains in depth. I’m far from a color expert, but I think I see what Brian (and Adam) are saying in that article. Say you have three different colors and they all have the exact same lightness in HSL. That doesn’t mean they are all actually the same lightness. That’s kinda weird, particularly when you’re using this color format as part of a system of colors.

The good news is that there are color features already specced as a CSS Level 4 module that help with this: Lab and LCH. Check out the example from Adam where the colors in Lab have values that reflect their actual lightness much more accurately to how we perceive it.

Brian:

There are color spaces like Lab and LCH which deal with the full spectrum and have qualities like perceptual uniformness. Thus, if we want great color functions for use in real design systems everyone seems to agree that having support to do said math in the Lab/LCH color spaces is the ideal enabling feature.

In the bug ticket for Chrome, Tab thinks these would be almost trivial to implement.

Note that lab()/lch()/gray() can all be eagerly converted into our existing color infrastructure; they don’t introduce any fundamentally new concepts, they’re just a better way to specify colors, more closely associated with how our eyes actually function rather than being closely tied to how rgb pixels function.

The conversion functions to turn it into rgb are a little bit of code, but it’s just some exponentials and a bit of matrix multiplication, and it’s well-documented in the spec.

This should be a GoodFirstBug sort of thing, I think.


The Best Color Functions in CSS? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-best-color-functions-in-css/feed/ 5 301297
Converting Color Spaces in JavaScript https://css-tricks.com/converting-color-spaces-in-javascript/ https://css-tricks.com/converting-color-spaces-in-javascript/#comments Thu, 10 Jan 2019 15:07:25 +0000 http://css-tricks.com/?p=280978 A challenge I faced in building an image “emojifier” was that I needed to change the color spaces of values obtained using getImageData() from RGB to HSL. I used arrays of emojis arranged by brightness and saturation, and they were …


Converting Color Spaces in JavaScript originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
A challenge I faced in building an image “emojifier” was that I needed to change the color spaces of values obtained using getImageData() from RGB to HSL. I used arrays of emojis arranged by brightness and saturation, and they were HSL-based for the best matches of average pixel colors with the emojis.

In this article, we’ll study functions that will be useful for converting both opaque and alpha-enabled color values. Modern browsers currently support the color spaces RGB(A), hex, and HSL(A). The functions and notations for these are rgb(), rgba(), #rgb/#rrggbb, #rgba/#rrggbbaa, hsl(), and hsla(). Browsers have always supported built-in names like aliceblue as well.

Balls with color values being inserted into a machine and coming out as HSL

Along the way, we’ll encounter use of some color syntaxes provided by a new Level 4 of the CSS Colors Module. For example, we now have hex with alpha as we mentioned (#rgba/#rrggbbaa) and RGB and HSL syntaxes no longer require commas (values like rgb(255 0 0) and hsl(240 100% 50%) became legal!).

Browser support for CSS Colors Level 4 isn’t universal as of this writing, so don’t expect new color syntaxes to work in Microsoft browsers or Safari if trying them in CSS.

RGB to Hex

Converting RGB to hex is merely a change of radices. We convert the red, green, and blue values from decimal to hexadecimal using toString(16). After prepending 0s to single digits and under, we can concatenate them and # to a single return statement.

function RGBToHex(r,g,b) {
  r = r.toString(16);
  g = g.toString(16);
  b = b.toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

Hex from RGB in String

Alternatively, we can use a single string argument with the red, green and blue separated by commas or spaces (e.g. "rgb(255,25,2)", "rgb(255 25 2)"). Substring to eliminate rgb(, split what’s left by the ), then split that result’s first item by whichever the separator (sep) is. r, g, and b shall become local variables now. Then we use + before the split strings to convert them back to numbers before obtaining the hex values.

function RGBToHex(rgb) {
  // Choose correct separator
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  // Turn "rgb(r,g,b)" into [r,g,b]
  rgb = rgb.substr(4).split(")")[0].split(sep);

  let r = (+rgb[0]).toString(16),
      g = (+rgb[1]).toString(16),
      b = (+rgb[2]).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

In addition, we can allow strings with channel values as percentages by adding the loop after redefining rgb. It’ll strip the %s and turn what’s left into values out of 255.

function RGBToHex(rgb) {
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  rgb = rgb.substr(4).split(")")[0].split(sep);

  // Convert %s to 0–255
  for (let R in rgb) {
    let r = rgb[R];
    if (r.indexOf("%") > -1)
      rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
      /* Example:
      75% -> 191
      75/100 = 0.75, * 255 = 191.25 -> 191
      */
  }

  ...
}

Now we can supply values like either of these:

  • rgb(255,25,2)
  • rgb(255 25 2)
  • rgb(50%,30%,10%)
  • rgb(50% 30% 10%)

RGBA to Hex (#rrggbbaa)

Converting RGBA to hex with the #rgba or #rrggbbaa notation follows virtually the same process as the opaque counterpart. Since the alpha (a) is normally a value between 0 and 1, we need to multiply it by 255, round the result, then convert it to hexadecimal.

function RGBAToHexA(r,g,b,a) {
  r = r.toString(16);
  g = g.toString(16);
  b = b.toString(16);
  a = Math.round(a * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

To do this with one string (including with percentages), we can follow what we did earlier. Also note the extra step of splicing out a slash. Since CSS Colors Level 4 supports the syntax of rgba(r g b / a), this is where we allow it. Alpha values can now be percentages! This removes the 0-1-only shackles we used to have. Therefore, the for loop cycling through rgba shall include a part to wipe the % from the alpha without multiplying by 255 (when R is 3 for alpha). Soon we can use values like rgba(255 128 0 / 0.8) and rgba(100% 21% 100% / 30%)!

function RGBAToHexA(rgba) {
  let sep = rgba.indexOf(",") > -1 ? "," : " "; 
  rgba = rgba.substr(5).split(")")[0].split(sep);
                
  // Strip the slash if using space-separated syntax
  if (rgba.indexOf("/") > -1)
    rgba.splice(3,1);

  for (let R in rgba) {
    let r = rgba[R];
    if (r.indexOf("%") > -1) {
      let p = r.substr(0,r.length - 1) / 100;

      if (R < 3) {
        rgba[R] = Math.round(p * 255);
      } else {
        rgba[R] = p;
      }
    }
  }
}

Then, where the channels are converted to hex, we adjust a to use an item of rgba[].

function RGBAToHexA(rgba) {
  ...
    
  let r = (+rgba[0]).toString(16),
      g = (+rgba[1]).toString(16),
      b = (+rgba[2]).toString(16),
      a = Math.round(+rgba[3] * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

Now the function supports the following:

  • rgba(255,25,2,0.5)
  • rgba(255 25 2 / 0.5)
  • rgba(50%,30%,10%,0.5)
  • rgba(50%,30%,10%,50%)
  • rgba(50% 30% 10% / 0.5)
  • rgba(50% 30% 10% / 50%)

Hex to RGB

We know that the length of hex values must either be 3 or 6 (plus #). In either case, we begin each red (r), green (g), and blue (b) value with "0x" to convert them to hex. If we provide a 3-digit value, we concatenate the same value twice for each channel. If it’s a 6-digit value, we concatenate the first two for red, next two for green, and last two for blue. To get the values for the final rgb() string, we prepend the variables with + to convert them from strings back to numbers, which will yield the decimals we need.

function hexToRGB(h) {
  let r = 0, g = 0, b = 0;

  // 3 digits
  if (h.length == 4) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];

  // 6 digits
  } else if (h.length == 7) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
  }
  
  return "rgb("+ +r + "," + +g + "," + +b + ")";
}

Output RGB with %s from Hex

If we want to return rgb() using percentages, then we can modify the function to utilize an optional isPct parameter like so:

function hexToRGB(h,isPct) {
  let r = 0, g = 0, b = 0;
  isPct = isPct === true;

  if (h.length == 4) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];
    
  } else if (h.length == 7) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
  }
    
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }
  
  return "rgb(" + (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")";
}

Under the last if statement, using +s will convert r, g, and b to numbers. Each toFixed(1) along with them will round the result to the nearest tenth. Additionally, we won’t have whole numbers with .0 or the decades old quirk that produces numbers like 0.30000000000000004. Therefore, in the return, we omitted the +s right before the first r, g, and b to prevent NaNs caused by the %s. Now we can use hexToRGB("#ff0",true) to get rgb(100%,100%,0%)!

Hex (#rrggbbaa) to RGBA

The procedure for hex values with alpha should again be similar with the last. We simply detect a 4- or 8-digit value (plus #) then convert the alpha and divide it by 255. To get more precise output but not long decimal numbers for alpha, we can use toFixed(3).

function hexAToRGBA(h) {
  let r = 0, g = 0, b = 0, a = 1;

  if (h.length == 5) {
    r = "0x" + h[1] + h[1];
    g = "0x" + h[2] + h[2];
    b = "0x" + h[3] + h[3];
    a = "0x" + h[4] + h[4];

  } else if (h.length == 9) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
    a = "0x" + h[7] + h[8];
  }
  a = +(a / 255).toFixed(3);

  return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")";
}

Output RGBA with %s from Hex

For a version that outputs percentages, we can do what we did in hexToRGB()—switch r, g, and b to 0–100% when isPct is true.

function hexAToRGBA(h,isPct) {
  let r = 0, g = 0, b = 0, a = 1;
  isPct = isPct === true;
    
  // Handling of digits
  ...

  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }
  a = +(a / 255).toFixed(3);

  return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a : +r + "," + +g + "," + +b + "," + a) + ")";
}

Here’s a quick fix if the alpha ought to be a percentage, too: move the statement where a is redefined above the last if statement. Then in that statement, modify a to be like r, g, and b. When isPct is true, a must also gain the %.

function hexAToRGBA(h,isPct) {
  ...
    
  a = +(a / 255).toFixed(3);
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
    a = +(a * 100).toFixed(1);
  }

  return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + "," + +g + "," + +b + "," + a) + ")";
}

When we enter #7f7fff80 now, we should get rgba(127,127,255,0.502) or rgba(49.8%,49.8%,100%,50.2%).

RGB to HSL

Obtaining HSL values from RGB or hex is a bit more challenging because there’s a larger formula involved. First, we must divide the red, green, and blue by 255 to use values between 0 and 1. Then we find the minimum and maximum of those values (cmin and cmax) as well as the difference between them (delta). We need that result as part of calculating the hue and saturation. Right after the delta, let’s initialize the hue (h), saturation (s), and lightness (l).

function RGBToHSL(r,g,b) {
  // Make r, g, and b fractions of 1
  r /= 255;
  g /= 255;
  b /= 255;

  // Find greatest and smallest channel values
  let cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;
}

Next, we need to calculate the hue, which is to be determined by the greatest channel value in cmax (or if all channels are the same). If there is no difference between the channels, the hue will be 0. If cmax is the red, then the formula will be ((g - b) / delta) % 6. If green, then (b - r) / delta + 2. Then, if blue, (r - g) / delta + 4. Finally, multiply the result by 60 (to get the degree value) and round it. Since hues shouldn’t be negative, we add 360 to it, if needed.

function RGBToHSL(r,g,b) {
  ...
  // Calculate hue
  // No difference
  if (delta == 0)
    h = 0;
  // Red is max
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  // Green is max
  else if (cmax == g)
    h = (b - r) / delta + 2;
  // Blue is max
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);
    
  // Make negative hues positive behind 360°
  if (h < 0)
      h += 360;
}

All that’s left is the saturation and lightness. Let’s calculate the lightness before we do the saturation, as the saturation will depend on it. It’s the sum of the maximum and minimum channel values cut in half ((cmax + cmin) / 2). Then delta will determine what the saturation will be. If it’s 0 (no difference between cmax and cmin), then the saturation is automatically 0. Otherwise, it’ll be 1 minus the absolute value of twice the lightness minus 1 (1 - Math.abs(2 * l - 1)). Once we have these values, we must convert them to values out of 100%, so we multiply them by 100 and round to the nearest tenth. Now we can string together our hsl().

function RGBToHSL(r,g,b) {
  ...
  // Calculate lightness
  l = (cmax + cmin) / 2;

  // Calculate saturation
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
    
  // Multiply l and s by 100
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

HSL from RGB in String

For one string, split the argument by comma or space, strip the %s, and localize r, g, and b like we did before.

function RGBToHSL(rgb) {
  let sep = rgb.indexOf(",") > -1 ? "," : " ";
  rgb = rgb.substr(4).split(")")[0].split(sep);

  for (let R in rgb) {
    let r = rgb[R];
    if (r.indexOf("%") > -1) 
      rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
  }

  // Make r, g, and b fractions of 1
  let r = rgb[0] / 255,
      g = rgb[1] / 255,
      b = rgb[2] / 255;

  ...
}

RGBA to HSLA

Compared to what we just did to convert RGB to HSL, the alpha counterpart will be basically nothing! We just reuse the code for RGB to HSL (the multi-argument version), leave a alone, and pass a to the returned HSLA. Keep in mind it should be between 0 and 1.

function RGBAToHSLA(r,g,b,a) {
  // Code for RGBToHSL(r,g,b) before return
  ...

  return "hsla(" + h + "," + s + "%," +l + "%," + a + ")";
}

HSLA from RGBA in String

For string values, we apply the splitting and stripping logic again but use the fourth item in rgba for a. Remember the new rgba(r g b / a) syntax? We’re employing the acceptance of it as we did for RGBAToHexA(). Then the rest of the code is the normal RGB-to-HSL conversion.

function RGBAToHSLA(rgba) {
  let sep = rgba.indexOf(",") > -1 ? "," : " ";
  rgba = rgba.substr(5).split(")")[0].split(sep);

  // Strip the slash if using space-separated syntax
  if (rgba.indexOf("/") > -1) 
    rgba.splice(3,1);

  for (let R in rgba) {
    let r = rgba[R];
    if (r.indexOf("%") > -1) {
      let p = r.substr(0,r.length - 1) / 100;

      if (R < 3) { 
        rgba[R] = Math.round(p * 255);
      } else {
        rgba[R] = p;
      }
    }
  }

  // Make r, g, and b fractions of 1
  let r = rgba[0] / 255,
      g = rgba[1] / 255,
      b = rgba[2] / 255,
      a = rgba[3];

  // Rest of RGB-to-HSL logic
  ...
}

Wish to leave the alpha as is? Remove the else statement from the for loop.

for (let R in rgba) {
  let r = rgba[R];
  if (r.indexOf("%") > -1) {
    let p = r.substr(0,r.length - 1) / 100;

    if (R < 3) {
      rgba[R] = Math.round(p * 255);
    }
  }
}

HSL to RGB

It takes slightly less logic to convert HSL back to RGB than the opposite way. Since we’ll use a range of 0–100 for the saturation and lightness, the first step is to divide them by 100 to values between 0 and 1. Next, we find chroma (c), which is color intensity, so that’s (1 - Math.abs(2 * l - 1)) * s. Then we use x for the second largest component (first being chroma), the amount to add to each channel to match the lightness (m), and initialize r, g, b.

function HSLToRGB(h,s,l) {
  // Must be fractions of 1
  s /= 100;
  l /= 100;

  let c = (1 - Math.abs(2 * l - 1)) * s,
      x = c * (1 - Math.abs((h / 60) % 2 - 1)),
      m = l - c/2,
      r = 0,
      g = 0,
      b = 0;
}

The hue will determine what the red, green, and blue should be depending on which 60° sector of the color wheel it lies.

Color wheel
The color wheel divided into 60° segments

Then c and x shall be assigned as shown below, leaving one channel at 0. To get the final RGB value, we add m to each channel, multiply it by 255, and round it.

function HSLToRGB(h,s,l) {
  ...

  if (0 <= h && h < 60) {
    r = c; g = x; b = 0;  
  } else if (60 <= h && h < 120) {
    r = x; g = c; b = 0;
  } else if (120 <= h && h < 180) {
    r = 0; g = c; b = x;
  } else if (180 <= h && h < 240) {
    r = 0; g = x; b = c;
  } else if (240 <= h && h < 300) {
    r = x; g = 0; b = c;
  } else if (300 <= h && h < 360) {
    r = c; g = 0; b = x;
  }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);

  return "rgb(" + r + "," + g + "," + b + ")";
}

RGB from HSL in String

For the single string version, we modify the first few statements basically the same way we did for RGBToHSL(r,g,b). Remove s /= 100; and l /= 100; and we’ll use the new statements to wipe the first 4 characters and the ) for our array of HSL values, then the %s from s and l before dividing them by 100.

function HSLToRGB(hsl) {
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);

  let h = hsl[0],
      s = hsl[1].substr(0,hsl[1].length - 1) / 100,
      l = hsl[2].substr(0,hsl[2].length - 1) / 100;

  ...
}

The next handful of statements shall handle hues provided with a unit—degrees, radians, or turns. We multiply radians by 180/π and turns by 360. If the result ends up over 360, we compound modulus divide to keep it within the scope. All of this will happen before we deal with c, x, and m.

function HSLToRGB(hsl) {
  ...

  // Strip label and convert to degrees (if necessary)
  if (h.indexOf("deg") > -1) 
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  // Keep hue fraction of 360 if ending up over
  if (h >= 360)
    h %= 360;
    
  // Conversion to RGB begins
  ...
}

After implementing the steps above, now the following can be safely used:

  • hsl(180 100% 50%)
  • hsl(180deg,100%,50%)
  • hsl(180deg 100% 50%)
  • hsl(3.14rad,100%,50%)
  • hsl(3.14rad 100% 50%)
  • hsl(0.5turn,100%,50%)
  • hsl(0.5turn 100% 50%)

Whew, that’s quite the flexibility!

Output RGB with %s from HSL

Similarly, we can modify this function to return percent values just like we did in hexToRGB().

function HSLToRGB(hsl,isPct) {
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);
  isPct = isPct === true;

  ...

  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
  }

  return "rgb("+ (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")";
}

HSLA to RGBA

Once again, handling alphas will be a no-brainer. We can reapply the code for the original HSLToRGB(h,s,l) and add a to the return.

function HSLAToRGBA(h,s,l,a) {
  // Code for HSLToRGB(h,s,l) before return
  ...

  return "rgba(" + r + "," + g + "," + b + "," + a + ")";
}

RGBA from HSLA in String

Changing it to one argument, the way we’ll handle strings here will be not too much different than what we did earlier. A new HSLA syntax from Colors Level 4 uses (value value value / value) just like RGBA, so having the code to handle it, we’ll be able to plug in something like hsla(210 100% 50% / 0.5) here.

function HSLAToRGBA(hsla) { 
  let sep = hsla.indexOf(",") > -1 ? "," : " ";
  hsla = hsla.substr(5).split(")")[0].split(sep);

  if (hsla.indexOf("/") > -1)
    hsla.splice(3,1);

  let h = hsla[0],
      s = hsla[1].substr(0,hsla[1].length - 1) / 100,
      l = hsla[2].substr(0,hsla[2].length - 1) / 100,
      a = hsla[3];
        
  if (h.indexOf("deg") > -1)
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  if (h >= 360)
    h %= 360;

  ...
}

Furthermore, these other combinations have become possible:

  • hsla(180,100%,50%,50%)
  • hsla(180 100% 50% / 50%)
  • hsla(180deg,100%,50%,0.5)
  • hsla(3.14rad,100%,50%,0.5)
  • hsla(0.5turn 100% 50% / 50%)

RGBA with %s from HSLA

Then we can replicate the same logic for outputting percentages, including alpha. If the alpha should be a percentage (searched in pctFound), here’s how we can handle it:

  1. If r, g, and b are to be converted to percentages, then a should be multiplied by 100, if not already a percentage. Otherwise, drop the %, and it’ll be added back in the return.
  2. If r, g, and b should be left alone, then remove the % from a and divide a by 100.
function HSLAToRGBA(hsla,isPct) {
  // Code up to slash stripping
  ...
    
  isPct = isPct === true;
    
  // h, s, l, a defined to rounding of r, g, b
  ...
    
  let pctFound = a.indexOf("%") > -1;
    
  if (isPct) {
    r = +(r / 255 * 100).toFixed(1);
    g = +(g / 255 * 100).toFixed(1);
    b = +(b / 255 * 100).toFixed(1);
    if (!pctFound) {
      a *= 100;
    } else {
      a = a.substr(0,a.length - 1);
    }
        
  } else if (pctFound) {
    a = a.substr(0,a.length - 1) / 100;
  }

  return "rgba("+ (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + ","+ +g + "," + +b + "," + +a) + ")";
}

Hex to HSL

You might think this one and the next are crazier processes than the others, but they merely come in two parts with recycled logic. First, we convert the hex to RGB. That gives us the base 10s we need to convert to HSL.

function hexToHSL(H) {
  // Convert hex to RGB first
  let r = 0, g = 0, b = 0;
  if (H.length == 4) {
    r = "0x" + H[1] + H[1];
    g = "0x" + H[2] + H[2];
    b = "0x" + H[3] + H[3];
  } else if (H.length == 7) {
    r = "0x" + H[1] + H[2];
    g = "0x" + H[3] + H[4];
    b = "0x" + H[5] + H[6];
  }
  // Then to HSL
  r /= 255;
  g /= 255;
  b /= 255;
  let cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;

  if (delta == 0)
    h = 0;
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  else if (cmax == g)
    h = (b - r) / delta + 2;
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);

  if (h < 0)
    h += 360;

  l = (cmax + cmin) / 2;
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

Hex (#rrggbbaa) to HSLA

There aren’t too many lines that change in this one. We’ll repeat what we recently did to get the alpha by converting the hex, but won’t divide it by 255 right away. First, we must get the hue, saturation, and lightness as we did in the other to-HSL functions. Then, before the ending return, we divide the alpha and set the decimal places.

function hexAToHSLA(H) {
  let r = 0, g = 0, b = 0, a = 1;

  if (H.length == 5) {
    r = "0x" + H[1] + H[1];
    g = "0x" + H[2] + H[2];
    b = "0x" + H[3] + H[3];
    a = "0x" + H[4] + H[4];
  } else if (H.length == 9) {
    r = "0x" + H[1] + H[2];
    g = "0x" + H[3] + H[4];
    b = "0x" + H[5] + H[6];
    a = "0x" + H[7] + H[8];
  }

  // Normal conversion to HSL
  ...
        
  a = (a / 255).toFixed(3);
                
  return "hsla("+ h + "," + s + "%," + l + "%," + a + ")";
}

HSL to Hex

This one starts as a conversion to RGB, but there’s an extra step to the Math.round()s of converting the RGB results to hex.

function HSLToHex(h,s,l) {
  s /= 100;
  l /= 100;

  let c = (1 - Math.abs(2 * l - 1)) * s,
      x = c * (1 - Math.abs((h / 60) % 2 - 1)),
      m = l - c/2,
      r = 0,
      g = 0, 
      b = 0; 

  if (0 <= h && h < 60) {
    r = c; g = x; b = 0;
  } else if (60 <= h && h < 120) {
    r = x; g = c; b = 0;
  } else if (120 <= h && h < 180) {
    r = 0; g = c; b = x;
  } else if (180 <= h && h < 240) {
    r = 0; g = x; b = c;
  } else if (240 <= h && h < 300) {
    r = x; g = 0; b = c;
  } else if (300 <= h && h < 360) {
    r = c; g = 0; b = x;
  }
  // Having obtained RGB, convert channels to hex
  r = Math.round((r + m) * 255).toString(16);
  g = Math.round((g + m) * 255).toString(16);
  b = Math.round((b + m) * 255).toString(16);

  // Prepend 0s, if necessary
  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

Hex from HSL in String

Even the first few lines of this function will be like those in HSLToRGB() if we changed it to accept a single string. This is how we’ve been obtaining the hue, saturation, and lightness separately in the first place. Let’s not forget the step to remove the hue label and convert to degrees, too. All of this will be in place of s /= 100; and l /= 100;.

function HSLToHex(hsl) { 
  let sep = hsl.indexOf(",") > -1 ? "," : " ";
  hsl = hsl.substr(4).split(")")[0].split(sep);

  let h = hsl[0],
      s = hsl[1].substr(0,hsl[1].length - 1) / 100,
      l = hsl[2].substr(0,hsl[2].length - 1) / 100;
        
  // Strip label and convert to degrees (if necessary)
  if (h.indexOf("deg") > -1)
    h = h.substr(0,h.length - 3);
  else if (h.indexOf("rad") > -1)
    h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));
  else if (h.indexOf("turn") > -1)
    h = Math.round(h.substr(0,h.length - 4) * 360);
  if (h >= 360)
    h %= 360;

  ...
}

HSLA to Hex (#rrggbbaa)

Adding alpha to the mix, we convert a to hex and add a fourth if to prepend a 0, if necessary. You probably already familiar with this logic because we last used it in RGBAToHexA().

function HSLAToHexA(h,s,l,a) {
  // Repeat code from HSLToHex(h,s,l) until 3 `toString(16)`s
  ...

  a = Math.round(a * 255).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;
  if (a.length == 1)
    a = "0" + a;

  return "#" + r + g + b + a;
}

Hex (#rrggbbaa) from HSLA in String

Finally, the lines of the single argument version up to a = hsla[3] are no different than those of HSLAToRGBA().

function HSLAToHexA(hsla) {
  let sep = hsla.indexOf(",") > -1 ? "," : " ";
  hsla = hsla.substr(5).split(")")[0].split(sep);
    
  // Strip the slash
  if (hsla.indexOf("/") > -1)
    hsla.splice(3,1);
    
  let h = hsla[0],
      s = hsla[1].substr(0,hsla[1].length - 1) / 100,
      l = hsla[2].substr(0,hsla[2].length - 1) / 100,
      a = hsla[3];
            
  ...
}

Built-in Names

To convert a named color to RGB, hex, or HSL, you might consider turning this table of 140+ names and hex values into a massive object at the start. The truth is that we really don’t need one because here’s what we can do:

  1. Create an element
  2. Give it a text color
  3. Obtain the value of that property
  4. Remove the element
  5. Return the stored color value, which will be in RGB by default

So, our function to get RGB will only be seven statements!

function nameToRGB(name) {
  // Create fake div
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  // Get color of div
  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  // Remove div after obtaining desired color value
  document.body.removeChild(fakeDiv);

  return pv;
}

Let’s go even further. How about we change the output to hex instead?

function nameToHex(name) {
  // Get RGB from named color in temporary div
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  document.body.removeChild(fakeDiv);

  // Code ripped from RGBToHex() (except pv is substringed)
  let rgb = pv.substr(4).split(")")[0].split(","),
      r = (+rgb[0]).toString(16),
      g = (+rgb[1]).toString(16),
      b = (+rgb[2]).toString(16);

  if (r.length == 1)
    r = "0" + r;
  if (g.length == 1)
    g = "0" + g;
  if (b.length == 1)
    b = "0" + b;

  return "#" + r + g + b;
}

Or, why not HSL? 😉

function nameToHSL(name) {
  let fakeDiv = document.createElement("div");
  fakeDiv.style.color = name;
  document.body.appendChild(fakeDiv);

  let cs = window.getComputedStyle(fakeDiv),
      pv = cs.getPropertyValue("color");

  document.body.removeChild(fakeDiv);

  // Code ripped from RGBToHSL() (except pv is substringed)
  let rgb = pv.substr(4).split(")")[0].split(","),
      r = rgb[0] / 255,
      g = rgb[1] / 255,
      b = rgb[2] / 255,
      cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;

  if (delta == 0)
    h = 0;
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  else if (cmax == g)
    h = (b - r) / delta + 2;
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);

  if (h < 0)
    h += 360;

  l = (cmax + cmin) / 2;
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}

In the long run, every conversion from a name becomes a conversion from RGB after cracking the name.

Validating Colors

In all these functions, there haven’t been any measures to prevent or correct ludicrous input (say hues over 360 or percentages over 100). If we’re only manipulating pixels on a fetched using getImageData(), validation of color values isn’t necessary before converting because they’ll be correct no matter what. If we’re creating a color conversion tool where users supply the color, then validation would be much needed.

It’s easy to handle improper input for channels as separate arguments, like this for RGB:

// Correct red
if (r > 255)
  r = 255;
else if (r < 0)
  r = 0;

If validating a whole string, then a regular expression is needed. For instance, this is the RGBToHex() function given a validation step with an expression:

function RGBToHex(rgb) {
  // Expression for rgb() syntaxes
  let ex = /^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i;

  if (ex.test(rgb)) {
    // Logic to convert RGB to hex
    ...

  } else {
    // Something to do if color is invalid
  }
}

To test other types of values, below is a table of expressions to cover both opaque and alpha-enabled:

Color ValueRegEx
RGB/^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i
RGBA/^rgba\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3}))|(((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){3}))\/\s)((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i
Hex/^#([\da-f]{3}){1,2}$/i
Hex (with Alpha)/^#([\da-f]{4}){1,2}$/i
HSL/^hsl\(((((([12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6](\.\d+)?)|(\.\d+))rad)((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}|(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2})\)$/i
HSLA/^hsla\(((((([12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6](\.\d+)?)|(\.\d+))rad)(((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)|((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

Looking at the expressions for RGB(A) and HSL(A), you probably have big eyes right now; these were made comprehensive enough to include most of the new syntaxes from CSS Colors Level 4. Hex, on the other hand, doesn’t need expressions as long as the others because of only digit counts. In a moment, we’ll dissect these and decipher the parts. Note that case-insensitive values (/i) pass all these.

RGB

/^rgb\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){2}|((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s)){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))|((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%))\)$/i

Because rgb() accepts either all integers or all percentages, both cases are covered. In the outmost group, between the ^rgb\( and \)$, there are inner groups for both integers and percentages, all comma-spaces or spaces only as separators:

  1. (((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?){2}|(((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){2})((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]))
  2. ((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){2}|((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){2})(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)

In the first half, we accept two instances of integers for red and green from 0–99 or 111-199 ((1?[1-9]?\d)), 100–109 (10\d), 200-249 ((2[0-4]\d)), or 250–255 (25[0-5]). We couldn’t simply do \d{1,3} because values like 03 or 017 and those greater than 255 shouldn’t be allowed. After that goes the comma and optional space (,\s?). On the other side of the |, after the first {2} (which indicates two instances of integers), we check for the same thing with space separators if the left side is false. Then for blue, the same should be accepted, but without a separator.

In the other half, acceptable values for percentages, including floats, should either be 0–99, explicitly 100 and not a float, or floats under 1 with the 0 dropped. Therefore, the segment here is (([1-9]?\d(\.\d+)?)|100|(\.\d+)), and it appears three times; twice with separator (,\s?){2}, %\s){2}), once without.

It is legal to use percentages without space separators (rgb(100%50%10%) for instance) in CSS, but the functions we wrote don’t support that. The same goes for rgba(100%50%10%/50%), hsl(40 100%50%), and hsla(40 100%50%/0.5). This could very well be a plus for code golfing and minification!

RGBA

/^rgba\((((((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?)){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3}))|(((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5])\s){3})|(((([1-9]?\d(\.\d+)?)|100|(\.\d+))%\s){3}))\/\s)((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

The next expression is very similar to the pervious, but three instances of integers (((((1?[1-9]?\d)|10\d|(2[0-4]\d)|25[0-5]),\s?){3})) or percentages ((((([1-9]?\d(\.\d+)?)|100|(\.\d+))%,\s?){3})), plus comma optional space are checked. Otherwise, it looks for the same thing but with space separators, plus a slash and space (\/\s) after the blue. Next to that is ((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%) where we accept floats with or without the first 0 ((0?\.\d+)), 0 or 1 ([01]) on the dot, or 0–100% ((([1-9]?\d(\.\d+)?)|100|(\.\d+))%).

Hex with Alpha

// #rgb/#rrggbb
/^#([\da-f]{3}){1,2}$/i
// #rgba/#rrggbbaa
/^#([\da-f]{4}){1,2}$/i

For both hex—with and without alpha—instances of numbers or letters a–f ([\da-f]) are accepted. Then one or two instances of this are counted for either short or longhand values supplied (#rgb or #rrggbb). As an illustration, we have this same short pattern: /^#([\da-f]{n}){1,2}$/i. Simply change n to 3 or 4.

HSL and HSLA

// HSL
/^hsl\((((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}|(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2})\)$/i
// HSLA
/^hsla\((((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)(((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)|((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))((0?\.\d+)|[01]|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)\)$/i

After the \( in both expressions for HSL and HSLA, this large chunk is for the hue:

(((((\[12]?[1-9]?\d)|[12]0\d|(3[0-5]\d))(\.\d+)?)|(\.\d+))(deg)?|(0|0?\.\d+)turn|(([0-6\\.\d+)?)|(\.\d+))rad)

([12]?[1-9]?\d) covers 0–99, 110–199, and 210–299. [12]0\d covers 110–109 and 200–209. Then (3[0-5]\d) takes care of 300–359. The reason for this division of ranges is similar to that of integers in the rgb() syntax: ruling out zeros coming first and values greater than the maximum. Since hues can be floating point numbers, the first (\.\d+)? is for that.

Next to the | after the aforementioned segment of code, the second (\.\d+) is for floats without a leading zero.

Now let’s move up a level and decipher the next small chunk:

(deg)?|(0|0?\.\d+)turn|((\[0-6\\.\d+)?)|(\.\d+))rad

This contains the labels we can use for the hue—degrees, turns, or radians. We can include all or none of deg. Values in turn must be under 1. For radians, we can accept any float between 0–7. We do know, however, that one 360° turn is 2π, and it stops approximately at 6.28. You may think 6.3 and over shouldn’t be accepted. Because 2π is an irrational number, it would be too messy for this example to try to satisfy every decimal place provided by the JavaScript console. Besides, we have this snippet in our HSLTo_() functions as a second layer of security if hues 360° or over were to happen:

// Keep hue fraction of 360 if ending up over
if (h >= 360)
  h %= 360;

Now let’s move up a level and decipher the second chunk:

(,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}

We’re counting two instances of comma-space-percentages for the saturation and lightness (space optional). In the group after the ,\s?, we test for values 0–99 with or without decimal points (([1-9]?\d(\.\d+)?)), exactly 100, or floats under 1 without the leading 0 ((\.\d+)).

The last part the HSL expression, before the ending (\)$/i), is a similar expression if spaces are the only separator:

(\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}

\s is in the beginning instead of ,\s?. Then in the HSLA expression, this same chunk is inside another group with ,\s? after its {2}.

((,\s?(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2},\s?)

That counts the comma-space between the lightness and alpha. Then if we have spaces as separators, we need to check for a space-slash-space (\s\/\s) after counting two instances of space and a percentage.

((\s(([1-9]?\d(\.\d+)?)|100|(\.\d+))%){2}\s\/\s))

After that, we have this left to check the alpha value:

(((0?\.\d+)|[01])|(([1-9]?\d(\.\d+)?)|100|(\.\d+))%)

Matches for (0?\.\d+) include floats under 1 with or without the leading 0, 0 or 1 for [01], and 0–100%.

Conclusion

If your current challenge is to convert one color space to another, you now have some ideas on how to approach it. Because it would be tiresome to walk through converting every color space ever invented in one post, we discussed the most practical and browser-supported ones. If you’d like to go beyond supported color spaces (say CMYK, XYZ, or CIE Lab*), EasyRGB) provides an amazing set of code-ready formulas.

To see all the conversions demonstrated here, I’ve set up a CodePen demo that shows inputs and outputs in a table. You can try different colors in lines 2–10 and see the complete functions in the JavaScript panel.


Converting Color Spaces in JavaScript originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/converting-color-spaces-in-javascript/feed/ 6 280978
Switch font color for different backgrounds with CSS https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/ https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/#comments Fri, 10 Aug 2018 13:58:59 +0000 http://css-tricks.com/?p=274679 Ever get one of those, “I can do that with CSS!” moments while watching someone flex their JavaScript muscles? That’s exactly the feeling I got while watching Dag-Inge Aas & Ida Aalen talk at CSSconf EU 2018.

They are …


Switch font color for different backgrounds with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Ever get one of those, “I can do that with CSS!” moments while watching someone flex their JavaScript muscles? That’s exactly the feeling I got while watching Dag-Inge Aas & Ida Aalen talk at CSSconf EU 2018.

They are based in Norway, where WCAG accessibility is not a just good practice, but actually required by law (go, Norway!). As they were developing a feature that allows user-selectable color theming for their main product, they faced a challenge: automatically adjusting the font color based on the selected background color of the container. If the background is dark, then it would be ideal to have a white text to keep it WCAG contrast compliant. But what happens if a light background color is selected instead? The text is both illegible and fails accessibility.

They used an elegant approach and solved the issue using the “color” npm package, adding conditional borders and automatic secondary color generation while they were at it.

But that’s a JavaScript solution. Here’s my pure CSS alternative.

The challenge

Here is the criteria I set out to accomplish:

  • Change the font color to either black or white depending on the background color
  • Apply the same sort of logic to borders, using a darker variation of the base color of the background to improve button visibility, only if background is really light
  • Automatically generate a secondary, 60° hue-rotated color

Working with HSL colors and CSS variables

The easiest approach I could think for this implies running HSL colors. Setting the background declarations as HSL where each parameter is a CSS custom property allows for a really simple way to determine lightness and use it as a base for our conditional statements.

:root {
  --hue: 220;
  --sat: 100;
  --light: 81;
}

.btn {
  background: hsl(var(--hue), calc(var(--sat) * 1%), calc(var(--light) * 1%));
}

This should allow us to swap the background to any color we’d like at runtime by changing the variables and running an if/else statement to change the foreground color.

Except… we don’t have if/else statements on CSS… or do we?

Introducing CSS conditional statements

Since the introduction of CSS variables, we also got conditional statements to go with them. Or sort of.

This trick is based on the fact that some CSS parameters get capped to a min and max value. For instance, think opacity. The valid range is 0 to 1, so we normally keep it there. But we can also declare an opacity of 2, 3, or 1000, and it will be capped to 1 and interpreted as such. In a similar fashion, we can even declare a negative opacity value, and get it capped to 0.

.something {
  opacity: -2; /* resolves to 0, transparent */
  opacity: -1; /* resolves to 0, transparent */
  opacity: 2; /*resolves to 1, fully opaque */
  opacity: 100; /* resolves to 1, fully opaque */
}

Applying the trick to our font color declaration

The lightness parameter of an HSL color declaration behaves in a similar way, capping any negative value to 0 (which results in black, whatever the hue and saturation happen to be) and anything above 100% is capped to 100% (which is always white).

So, we can declare the color as HSL, subtract the desired threshold from the lightness parameter, then multiply by 100% to force it to overshoot one of the limits (either sub-zero or higher than 100%). Since we need negative results to resolve in white and positive results to resolve in black, we also have to invert it multiplying the result by -1.

:root {
  --light: 80;
  /* the threshold at which colors are considered "light." Range: integers from 0 to 100,
recommended 50 - 70 */
  --threshold: 60;
}

.btn {
  /* Any lightness value below the threshold will result in white, any above will result in black */
  --switch: calc((var(--light) - var(--threshold)) * -100%);
  color: hsl(0, 0%, var(--switch));
}

Let’s review that bit of code: starting from a lightness of 80 and considering a 60 threshold, the subtraction results in 20, which multiplied by -100%, results in -2000% capped to 0%. Our background is lighter than the threshold, so we consider it light and apply black text.

If we had set the --light variable as 20, the subtraction would have resulted in -40, which multiplied by -100% would turn 4000%, capped to 100%. Our light variable is lower than the threshold, therefore we consider it a “dark” background and apply white text to keep a high contrast.

Generating a conditional border

When the background of an element becomes too light, it can get easily lost against a white background. We might have a button and not even notice it. To provide a better UI on really light colors, we can set a border based on the very same background color, only darker.

A light background with a dark border based on that background color.

To achieve that, we can use the same technique, but apply it to the alpha channel of an HSLA declaration. That way, we can adjust the color as needed, then have either fully transparent or fully opaque.

:root {
  /* (...) */
  --light: 85;
  --border-threshold: 80;
}

.btn {
  /* sets the border-color as a 30% darker shade of the same color*/
  --border-light: calc(var(--light) * 0.7%);
  --border-alpha: calc((var(--light) - var(--border-threshold)) * 10);
  
  border: .1em solid hsla(var(--hue), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}

Assuming a hue of 0 and saturation at 100%, the above code will provide a fully opaque, pure red border at 70% the original lightness if the background lightness is higher than 80, or a fully transparent border (and therefore, no border at all) if it’s darker.

Setting the secondary, 60° hue-rotated color

Probably the simplest of the challenges. There are two possible approaches for it:

  1. filter: hue-rotate(60): This is the first that comes to mind, but it’s not the best solution, as it would affect the color of the child elements. If necessary, it can be reversed with an opposite rotation.
  2. HSL hue + 60: The other option is getting our hue variable and adding 60 to it. Since the hue parameter doesn’t have that capping behavior at 360 but instead wraps around (as any CSS <angle> type does), it should work without any issues. Think 400deg=40deg, 480deg=120deg, etc.

Considering this, we can add a modifier class for our secondary-colored elements that adds 60 to the hue value. Since self-modifying variables are not available in CSS (i.e. there’s no such thing as --hue: calc(var(--hue) + 60) ), we can add a new auxiliary variable for our hue manipulation to our base style and use it in the background and border declaration.

.btn {
  /* (...) */
  --h: var(--hue);
  background: hsl(var(--h), calc(var(--sat) * 1%), calc(var(--light) * 1%));
  border:.1em solid hsla(var(--h), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}

Then re-declare its value in our modifier:

.btn--secondary {
  --h: calc(var(--hue) + 60);
}

The best thing about this approach is that it’ll automatically adapt all our calculations to the new hue and apply them to the properties, because of CSS custom properties scoping.

And there we are. Putting it all together, here’s my pure CSS solution to all three challenges. This should work like a charm and save us from including an external JavaScript library.

See the Pen CSS Automatic switch font color depending on element background…. FAIL by Facundo Corradini (@facundocorradini) on CodePen.

Except it doesn’t. Some hues get really problematic (particularly yellows and cyans), as they are displayed way brighter than others (e.g. reds and blues) despite having the same lightness value. In consequence, some colors are treated as dark and given white text despite being extremely bright.

What in the name of CSS is going on?

Introducing perceived lightness

I’m sure many of you might have noticed it way ahead, but for the rest of us, turns out the lightness we perceive is not the same as the HSL lightness. Luckily, we have some methods to weigh in the hue lightness and adapt our code so it responds to hue as well.

To do that, we need to take into consideration the perceived lightness of the three primary colors by giving each a coefficient corresponding to how light or dark the human eye perceives it. This is normally referred to as luma.

There are several methods to achieve this. Some are better than others in specific cases, but not one is 100% perfect. So, I selected the two most popular, which are good enough:

  • sRGB Luma (ITU Rec. 709): L = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255
  • W3C method (working draft): L = (red * 0.299 + green * 0.587 + blue * 0.114) / 255

Implementing luma-corrected calculations

The first obvious implication of going with a luma-corrected approach is that we cannot use HSL since CSS doesn’t have native methods to access the RGB values of an HSL declaration.

So, we need to switch to an RBG declaration for the backgrounds, calculate the luma from whatever method we choose, and use it on our foreground color declaration, which can (and will) still be HSL.

:root {
  /* theme color variables to use in RGB declarations */
  --red: 200;
  --green: 60;
  --blue: 255;
  /* the threshold at which colors are considered "light". 
  Range: decimals from 0 to 1, recommended 0.5 - 0.6 */
  --threshold: 0.5;
  /* the threshold at which a darker border will be applied.
  Range: decimals from 0 to 1, recommended 0.8+ */
  --border-threshold: 0.8;
}

.btn {
  /* sets the background for the base class */
  background: rgb(var(--red), var(--green), var(--blue));

  /* calculates perceived lightness using the sRGB Luma method 
  Luma = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255 */
  --r: calc(var(--red) * 0.2126);
  --g: calc(var(--green) * 0.7152);
  --b: calc(var(--blue) * 0.0722);
  --sum: calc(var(--r) + var(--g) + var(--b));
  --perceived-lightness: calc(var(--sum) / 255);
  
  /* shows either white or black color depending on perceived darkness */
  color: hsl(0, 0%, calc((var(--perceived-lightness) - var(--threshold)) * -10000000%)); 
}

For the conditional borders, we need to turn the declaration into RGBA, and once again, use the alpha channel to make it either fully transparent or fully opaque. Pretty much the same thing as before, only running RGBA instead of HSLA. The darker shade is obtained by subtracting 50 from each channel.

There’s a really weird bug on WebKit on iOS that will show a black border instead of the appropriate color if using variables in the RGBA parameters of the shorthand border declaration. The solution is declaring the border in longhand.
Thanks Joel for pointing out the bug!

.btn {
  /* (...) */
  /* applies a darker border if the lightness is higher than the border threshold */ 
  --border-alpha: calc((var(--perceived-lightness) - var(--border-threshold)) * 100);

  border-width: .2em;
  border-style: solid;
  border-color: rgba(calc(var(--red) - 50), calc(var(--green) - 50), calc(var(--blue) - 50), var(--border-alpha));  
}

Since we lost our initial HSL background declaration, our secondary theme color needs to be obtained via hue rotation:

.btn--secondary {
  filter: hue-rotate(60deg);
}

This is not the best thing in the world. Besides applying the hue rotation to potential child elements as previously discussed, it means the switch to black/white and border visibility on the secondary element will depend on the main element’s hue and not on its own. But as far as I can see, the JavaScript implementation has the same issue, so I’ll call it close enough.

And there we have it, this time for good.

See the Pen CSS Automatic WCAG contrast font-color depending on element background by Facundo Corradini (@facundocorradini) on CodePen.

A pure CSS solution that achieves the same effect as the original JavaScript approach, but significantly cuts on the footprint.

Browser support

IE is excluded due to the use of CSS variables. Edge doesn’t have that capping behavior we used throughout. It sees the declaration, deems it nonsense, and discards it altogether as it would any broken/unknown rule. Every other major browser should work.


Switch font color for different backgrounds with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/feed/ 20 274679
HSL() / HSLa() is great for programmatic color control https://css-tricks.com/hsl-hsla-is-great-for-programmatic-color-control/ https://css-tricks.com/hsl-hsla-is-great-for-programmatic-color-control/#comments Fri, 01 Jun 2018 21:51:04 +0000 http://css-tricks.com/?p=270720 If you ever need to hand-manipulate a color in native CSS, HSL is pretty much the only way. HSL (the hsl() and hsla() functions in CSS) stands for hue, saturation, lightness, and optionally, alpha. We’ve talked about it before but …


HSL() / HSLa() is great for programmatic color control originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
If you ever need to hand-manipulate a color in native CSS, HSL is pretty much the only way. HSL (the hsl() and hsla() functions in CSS) stands for hue, saturation, lightness, and optionally, alpha. We’ve talked about it before but we can break it down a little more and do some interesting things with it.

  • Hue: Think of a color wheel. Around 0o and 360o are reds. 120o is where greens are and 240o are blues. Use anything in between 0-360. Values above and below will be modulus 360.
  • Saturation: 0% is completely desaturated (grayscale). 100% is fully saturated (full color).
  • Lightness: 0% is completely dark (black). 100% is completely light (white). 50% is average lightness.
  • alpha: Opacity/Transparency value. 0 is fully transparent. 1 is fully opaque. 0.5 is 50% transparent.

You can hand-manipulate any of those four values and have a decent sense of what is going to happen. Change the hue to take a trip around the color wheel. Change the saturation to get deeper or more muted colors. Change the lightness to essentially mix in black or white.

You might have some mental chops with rgb(), knowing that rgb(255, 0, 0) is clearly red or rgb(0, 0, 0) is black, but manipulating those to get to a light purple or starting with a forest green and getting a little lighter isn’t exactly mental math. You might even be the clever sort who can identify color by Hex codes. Ask David DeSandro at a party sometime. Still, nothing nearly as intuitive as HSL.

Those of you on the cutting edge might recall the working draft of Color Level 4 with the color() function and more intuitive sub-functions. Or, you might get hot and heavy with Sass color functions or your own home brew thing. More power to ya!

See the Pen Sass Color Functions by Chris Coyier (@chriscoyier) on CodePen.

I really like HSL when playing with color in JavaScript. For example, say you want to generate some different red tones. You could randomize the H, S, and L tightly around some values:

See the Pen Random Reds by Chris Coyier (@chriscoyier) on CodePen.

Not long ago, I basically did the same thing but rotated the hue to animate this stargate:

See the Pen Sparklegate by Chris Coyier (@chriscoyier) on CodePen.

If you’re messing with color in JavaScript and want randomization to result in pleasing colors, give PleaseJS a spin.

Need a quick color picker? I put this one together ages ago and I quickly ported it over to CodePen:

See the Pen HSL Explorer by Chris Coyier (@chriscoyier) on CodePen.

There is also the HSL Color Picker and Mothereffering HSL if you want options.

See the Pen HSL by Graham Pyne (@gpyne) on CodePen.

Wanna learn more about color on the web in general? Don’t miss Sarah Drasner’s A Nerd’s Guide to Color on the Web. Lots of great goodies in there to up your understanding of working with color.

See the Pen HSL by Dan Wilson (@danwilson) on CodePen.


HSL() / HSLa() is great for programmatic color control originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/hsl-hsla-is-great-for-programmatic-color-control/feed/ 1 270720
A Nerd’s Guide to Color on the Web https://css-tricks.com/nerds-guide-color-web/ https://css-tricks.com/nerds-guide-color-web/#comments Mon, 12 Sep 2016 13:50:54 +0000 http://css-tricks.com/?p=245208 There are a lot of ways to work with color on the web. I think it’s helpful to understand the mechanics behind what you’re using, and color is no exception. Let’s delve into some of the technical details of color …


A Nerd’s Guide to Color on the Web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
There are a lot of ways to work with color on the web. I think it’s helpful to understand the mechanics behind what you’re using, and color is no exception. Let’s delve into some of the technical details of color on the web.

Color mixing

A huge part of working with color is understanding that the way that you used color as a child doesn’t work the same as how you use color on a computer because of color mixing. As a child, you’re working with paint. Paint and inks from a printer have tiny particles called pigments that mix together and reflect to present color to your eye. This is subtractive color mixing. The more colors you add to it, the darker it becomes, until we get brown. Primaries are close to what you’re used to: red, yellow, blue. But when you mix these colors with subtractive color mixing, you arrive at brown.

On a computer (or any monitor), we’re working with light. Which means that when all of the colors mix together, they make white. Before Isaac Newton’s famous prism color experiment, color was believed to be contained within objects rather than reflected and absorbed from the object. Isaac Newton used a prism to prove his theory that sunlight or bright white light was in fact several colors by using the prism to split apart the colors to make a rainbow, and then subsequently using a prism to attempt to further split the blue. The blue did not split, showing that the color wasn’t within the prism, but rather that the prism was splitting the light. This means that in additive color mixing, the type of color mixing you get in a monitor, red green and blue can be used to produce all colors, or rgb. In this type of mixing, red and green create yellow.

Monitors are many combinations of small bits of light combined that resonate to create a myriad of colors. Resolution refers to the number of individual dots of color, known as pixels, contained on a display. Before we had monitors, artists were using this type of light frequency. Seurat and the Pointillists used red and green to create yellow in paintings like “La Grande Jatte” (though he preferred the term chromo-luminarism. Others called it divisionism) This type of painting was created under the belief that optical mixing created more pure resonance in your eye that traditional subtractive pigment color mixing.

Monitors are made in a few different display modes that change the way we perceive color through them. We express this the term “color bit depth”. The number of colors that can be displayed at one time is determined by this color bit depth. If we have a bit depth of 1, we can produce two colors, or monochrome. Bit depth of two levels creates 4, and so on until we reach a bit-depth of 32, though commonly monitors that project the web have 24 bit-depth density and 16,777,216 colors which is True Color and Alpha Channel.

We call this True Color because our human eyes can discern 10,000,000 unique colors, so 24-bit depth will certainly allow for this. In this 24-bit depth, 8 bits are dedicated to red, green, and blue. The rest are used for transparency or alpha channels.

Let’s use this information to unpack our available color properties on the web.

Color values

RGB Values

The last section illustrates what rbga(x, x, x, y); communicates, but let’s break that down a bit more, and show some other properties and their uses. In terms of web color values in an RGB channel, we specify color on a range from 0-255.

x is a number from 0-255
y is a number from 0.0 to 1.0
rgb(x, x, x); or rgba(x, x, x, y);

Example: rbga(150, 150, 150, 0.5);

Hex Values

Hex colors are a slightly different format to represent the values in the same way. Hex values are probably the most common way developers designate color on the web.

If you recall that a byte is 8 bits, each Hex color or number represents a byte. A color is specified according to the intensity of its red, green and blue components, so we call it a triplet, with each expressed in two places. One byte represents a number in the range 00 to FF (in hexadecimal notation), or 0 to 255 in decimal notation. Byte 1 is Red, byte 2 is green, and byte 3 is blue. Hexadecimal is named this because it uses a base 16 system. The values use ranges from 0-9 and A-F, 0 being the lowest value and F being the highest, or #00000 being black and #FFFFFF being white.

For triplets with repeated values, you can eliminate the repetition by writing in shorthand, for instance, #00FFFF becomes #0FF. This system is easy for computers to understand, and it pretty short to write, which makes it useful for quick copy paste and designation in programming. If you’re going to work with colors in a more involved way, though, HSL is a little bit more human-readable.

HSL Values

Hsl values work with similar semantics and ranges as rgb, but rather than working with values as the monitor interprets the colors, hsl values work with hue, saturation, lightness values. This looks syntactically similar to rgb values but the ranges are different. This system is based on a Munsell color system (he was the first to separate out color into these three channels, or create a three dimensional system based on mathematical principles tied to actual human vision).

Hue, Saturation and Lightness can be represented as a three-dimensional model.

Hue rotates in 360 degrees, a full circle, while saturation and lightness are percentages from 0 to 100.

x is a number from 0 - 360
y is a percentage from 0% to 100%
z is a number from 0.0 to 1.0
hsl(x, y, y); or hsla(x, y, y, z);

Example: hsla(150, 50%, 50%, 0.5);

It’s a relatively easy change (around 11 lines of code, to be precise) for the browsers to exchange between rgb and hsl values, but for us humans, the use of hsl can be a lot easier to interpret. Imagine a wheel, with dense and saturated content at the center. This demo does a pretty good job of showing how it’s expressed.

Chris also made a nifty tool a few years back called the hsla explorer, which you can check out here.

If you don’t feel particularly skilled working with color, hsla() allows for some pretty simple rules to create nice effects for developers. We cover more about this in the generative color section below.

Named Colors

Named colors are also available to us as developers. Named colors, though, have a reputation for being difficult to work with due to their imprecision. The most notable and “famous” examples are that dark grey is actually lighter than grey and lime and limegreen are totally different colors. There’s even a game where you can try to guess named colors on the web, made by Chris Heilmann. Back in the old days, chucknorris was a blood red color (it’s only supported in HTML now as far as I can tell), but that was my favorite. Named colors can be useful for demonstrating color use quickly, but typically developers use Sass or other preprocessors to store color values by hex, rgba, or hsla and map them to color names used within the company.

Color Variables

A good practice is to store color variables and never use them directly, mapping them instead to other variables with more semantic naming schemes. CSS has native variables, like:

:root {
  --brandColor: red;
}

body {
  background: var(--brandColor);
}

But they are fairly new and haven’t made their way into Microsoft browsers at the time of this writing.

CSS preprocessors also support variables, so you can set up variables like $brandPrimary to use through your code base. Or a map:

$colors: (
  mainBrand: #FA6ACC,
  secondaryBrand: #F02A52,
  highlight: #09A6E4
);

@function color($key) {
  @if map-has-key($colors, $key) {
    @return map-get($colors, $key);
  }

  @warn "Unknown `#{$key}` in $colors.";
  @return null;
}

// _component.scss
.element {
  background-color: color(highlight); // #09A6E4
}

Remember that naming is important here. Abstract naming is sometimes useful so that if you change a variable that was representing a blue color to an orange color, you don’t have to go through and rename all of your color values. Or worse yet, put up a sign that says “$blue is orange now.” *sad trombone noise*

currentColor

currentColor is an incredibly useful value. It respects the cascade, and is useful for extending a color value to things like box shadows, outlines, borders, or even backgrounds.

Let’s say you have created a div and then inside it another div. This would create orange borders for the internal div:

.div-external { color: orange; }
.div-internal { border: 1px solid currentColor; }

This is incredibly useful for icon systems, either SVG icons for icon fonts. You can set currentColor as the default for the fill, stroke, or color, and then use semantically appropriate CSS classes to style the sucker.

Preprocessors

CSS preprocessors are great for tweaking colors. Here’s some links to different preprocessors documentation on color functions:

Here are a few of the cool things we can specifically with Sass:

mix($color1, $color2, [$weight])
adjust-hue($color, $degrees)
lighten($color, $amount)
darken($color, $amount)
saturate($color, $amount)

There are truthfully dozens of ways to programmatically mix and alter colors with preprocessors, and we won’t go into depth for all of them, but here’s a great interactive resource for more in-depth information.

Color Properties

Color, as a CSS property, refers to font color. If you’re setting a color on a large area, you would use background-color, unless it’s an SVG element in which case you would use fill. Border is the border around an HTML element, while stroke is it’s SVG counterpart.

Box and Text Shadows

The box-shadow and text-shadow properties accept a color value. Text shadows accept 2-3 values, h-shadow (horizontal shadow), v-shadow (vertical shadow), and an optional blur-radius. Box shadows take 2-4 values, h-shadow, v-shadow, optional blur distance, and optional spread distance. You can also designate inset at the start to create an inverted shadow. This site has a great demo with easy, pasteable code.

Gradients

Linear gradients work by designating a direction. From/to (depending on the browser prefix) top, bottom, left, right, degrees, or radial-gradients. We then specify color stops and the color we want at each stop. These can accept transparency too.

Here’s an example:

Most of the syntax of gradients isn’t all that difficult to write, but I really enjoy working with this online gradient generator, because it also creates the complicated filter property for IE6-9 support. Here is also a really beautiful UI gradient creator. This one is pretty cool and it is open source and you can contribute to it.

Gradients are similarly easy to create in SVG. We define a block that you reference with an id. We can optionally define a surface area for the gradient as well.

<linearGradient id="Gradient">
  <stop id="stop1" offset="0" stop-color="white" stop-opacity="0" />
  <stop id="stop2" offset="0.3" stop-color="black" stop-opacity="1" />
</linearGradient>

These gradients also support opacity so we can have some nice effects and layer effects like animate them as as a mask.

Gradient text is also possible in webkit only, we have a really a nice code snippet for that here on CSS-Tricks.

Generative Color

There are a few cool ways to drum up a lot of staggering colors at once. I find these to be really fun to play with when creating generative art or UI elements with code.

As long as you stay within the ranges designated in the last sections, you can use for loops in either Sass (or any CSS preprocessor) or JavaScript, or Math.Random() with Math.floor() to retrieve color values. We need Math.floor() or Math.ceil() here because if we don’t return full integers, we’ll get an error and do not get a color value.

A good rule of thumb is that you shouldn’t update all three values. I’ve had good luck with a lot of deviation in one range of values, a smaller deviation in the second set of values, and no deviation for the third, not necessarily in that order. For instance, hsl is very easy to work with to step through color because you know that looping through the hue from 0 to 360 will give you a full range. Another nice grace of hue-rotate in degrees is that because it’s a full circle, you don’t need to stick to ranges of 0 – 360, even -480 or 600 is still a value a browser can interpret.

Sass

@mixin colors($max, $color-frequency) {
  $color: 300/$max;
  
  @for $i from 1 through $max {
    .s#{$i} {
      border: 1px solid hsl(($i - 10)*($color*1.25), ($i - 1)*($color / $color-frequency), 40%);
     }
  }
} 
.demo {
  @include colors(20,2);
}

I use that to make fruit loop colors in this demo:

As well as this one, with a different range (scroll inside the list really fast):

In the code below, I’m using Math.random() within rgb values to drum up a lot of color within the same range. This demo is creating a three-dimensional VR experience with React. I could have stepped through it with a for loop as well, but I wanted the color to be randomized to reflect the movement. Sky’s the limit on this one.

Click on the image to see the full demo.

JavaScript

class App extends React.Component {
  render () {
    const items = [],
          amt1 = 5,
          amt2 = 7;
    for (let i = 0; i < 30; i++) {
     let rando = Math.floor(Math.random() * (amt2 - 0 + 1)) + 0,
          addColor1 = parseInt(rando * i),
          addColor2 = 255 - parseInt(7 * i),
          updateColor = `rgb(200, ${addColor1}, ${addColor2})`;
      items.push(
	    // ...
        );
    }
    return (
      
       // ...
       {items}
      
    );
  }
}

GreenSock came out with a tool that allows you to animate relative color values, which is useful because it means you can grab a lot of elements at once and animate them relative to their current color coordinates. Here are some turtles that demonstrate the idea:

TweenMax.to(".turtle2 path, .turtle2 circle, .turtle2 ellipse", 1.5, {fill:"hsl(+=0, +=50%, +=0%)"});

Other Nice Color Effects

Mix Blend Modes and Background Blend Modes

If you’ve used layer effects in Photoshop, you’re probably familiar with mix blend modes. Almost every site in the 90s used them (mine did. *blush*). Mix and background blend modes composite two different layered images together, and there are 16 modes available. Going through each is beyond the scope of this article, but here are some key examples.

The top image or color is called the source, and the bottom layer is called the destination. The area between the two is where the blending magic happens and is called the backdrop. We’re mixing both according to fairly simple mathematical formulas.

If you want to get really nerdy with me, the color formulas for the blend modes depend on the type of effect used. For instance, multiply is destination × source = backdrop. Other effects are variations of simple math using subtraction, multiplication, addition, and division. Linear is A+B−1, while Color Burn is 1−(1−B)÷A. You don’t really need to know any of these to use them, though.

Here is some more extensive documentation, and here’s a very simple demo to illustrate color working with some of these effects:

This great article by Robin demonstrates some really complex and impressive effects you can achieve from layering multiple blend modes as well. Below we’ll cover mixing them with filters. There’s really a lot you can do in the browser these days.

Filters

CSS Filters provide a lot of cool color effects, as well as the ability to take a colored image and make it greyscale. We have a great resource here on CSS-Tricks that shows how these work, and the browser support is pretty high now. Bennett Feely also has this nice filter gallery if you’re feeling explorative.

Filters and Blur modes can work together! Una Kravets created this cool tool called CSS Gram combining some effects to create typical instagram filters, she has some nice documentation at the bottom.

feColorMatrix

Una has another article exploring the creation of these images with feColorMatrix instead, which is a filter primitive in SVG that can be applied to HTML elements as well. It’s very powerful, and allows you to fine-tune and finesse color. As the name implies, the base markup of feColorMatrix uses a matrix of values, and we apply it using it’s relative id.

<filter id="imInTheMatrix">
    <feColorMatrix in="SourceGraphic"
      type="matrix"
      values="0 0 0 0 0
              1 1 1 1 0
              0 0 0 0 0
              0 0 0 1 0" />
  </filter>

  <path filter="url(#imInTheMatrix)"  … />

We can also extend this matrix and adjust the hue, saturation, etc, of these values:

<filter id="imInTheHueMatrix">
  <feColorMatrix in="SourceGraphic"
    type="hueRotate"
    values="150" />
</filter>

Una’s article goes into depth exploring all of the capabilities here, but you can get even more information on this and a lot of other crazy SVG colors and gradient tools with Amelia Belamy-Royd’s O’Reilly Book, SVG Colors, Patterns & Gradients or Mike Mullany’s exploratory demo.

Accessibility and Other Things to Note about Color

A color is only a color in reference to another color. This is part of what makes color so difficult. You’re probably a little familiar with this in terms of accessibility. A light green on a black may be accessible, but when you change it to a white background it no longer is.

Accessibility in color can be measured with a number of tools. Here are some of my favorites:

It’s also really nice to set up your palette for accessibility from the start. Color Safe is a great tool that helps with that. Once you’re all set up, WAVE (Web Accessibility Tool) will help you evaluate your web page:

Color and Atmosphere

Color is affected by atmosphere, which is a pretty important thing to know if you’re going to create any kind of illusion of depth. Things that are closer to you are in higher saturation, and in more contrast. Things that are further away from you are going to look blurrier.

Landscape showing color contrasts from things closer and farther away

Shadows

Shadows are not grey, they are the compliment of the color of the light. If the light you shine on your hand has a yellow cast, the shadow will appear purple. This is useful knowledge if you’re making any super hip long shadows.

Shadow is the compliment of the color

Native Color Inputs

There is a native browser color selector that you can employ to help your users select colors dynamically. You can write or if you’d like to start off with color hinting. It’s that simple to use. Good job, browsers. One thing to keep in mind is that the way that it will appear will vary slightly from browser to browser, just like any other native controls. This pen from Noah Blon shows how to use that in tandem with a hue CSS color filter to dynamically select portions of an image to change the color of. The rest of image is greyscale, so it’s not affected. Pretty clever.

Fun Developer Stuff and Other Resources

  • The Color Highlighter Plugin for Sublime Text is what I use to easily see what color the browser is going to interpret. I like to use {"ha_style": "outlined"} but I know from this article that Wes Bos prefers “filled”.
  • There are some different traditional palette combinations, and online web resources that can help you drum these up. For the more scientific, Paletton or Adobe Color. Benjamin Knight recreated Adobe’s color tool in d3 on CodePen, which is pretty badass and worth checking out. If you want the web to do the heavy lifting for you (who doesn’t?), Coolors is as simple as can be.
  • If you need help interpreting colors, and want a quick simple tool to exchange types of color properties for you, Colorhexa has you covered in pretty much every type of color exchange you can think of.
  • For the nerdiest of color nerds, you can even have your console output in colors to you. Here’s a great Pen showing how that works.
  • Super Color Palette is a little playground for creating color combinations with a variety of controls and the ability to export colors in differnt image formats, including SVG, JPG, and PNG. It’s a free project with a Discord channel to share your color palettes and talk color nerdery.

Conclusion

The scope of this article is rather large, and the web has a lot of color to delve into, but hopefully this short article gives you a jumping off point for some experimentation and understanding.


A Nerd’s Guide to Color on the Web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/nerds-guide-color-web/feed/ 16 245208
Yay for HSLa https://css-tricks.com/yay-for-hsla/ https://css-tricks.com/yay-for-hsla/#comments Tue, 15 Jun 2010 13:09:20 +0000 http://css-tricks.com/?p=6565 Huge sogging longbottoms? High strength low alloy steel? NOPE, we’re talking Hue, Saturation, Lightness, and alpha, and it’s a way of declaring color in CSS. It looks like this:

#some-element {
  background-color: hsla(170, 50%, 45%, 1);
}

It is similar …


Yay for HSLa originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Huge sogging longbottoms? High strength low alloy steel? NOPE, we’re talking Hue, Saturation, Lightness, and alpha, and it’s a way of declaring color in CSS. It looks like this:

#some-element {
  background-color: hsla(170, 50%, 45%, 1);
}

It is similar to RGBa in that you declare three values determining the color and then a fourth value for its transparency level. You can read more about browser support below, but it’s basically any browser that supports rgba() supports hsla() too.

There’s a new syntax for HSL snd RGB colors! Now, there’s no need for commas or the “a” in either function to set an alpha transparency. Instead, we use a forward slash (/) in front of the alpha value. E.g. hsl(170 50% 45% / 1) instead of hsla(170, 50%, 45%, 1). Read more about the change and other updated CSS color features.

  • Hue – Think of a color wheel. Around 0o and 360o are reds 120o are greens, 240o are blues. Use anything in between 0-360. Values above and below will be modulus 360.
  • Saturation – 0% is completely denatured (grayscale). 100% is fully saturated (full color).
  • Lightness – 0% is completely dark (black). 100% is completely light (white). 50% is average lightness.
  • alpha – Opacity/Transparency value. 0 is fully transparent. 1 is fully opaque. 0.5 is 50% transparent.

Let’s dig in.

Why is it cool?

The real appeal of HSLa is that it makes more intuitive sense what changing the values will do to the color. Increasing the second value will increase the saturation of that color. Decreasing the third value will decrease the lightness of that color. That makes creating your own color variations on the fly way easier. I would wager that most of us can’t create nice and consistent color variations like this using the RGBa model.

The HSLa model also makes changing color values programatically much easier. Check out the demo tool emphasizing that.

Why is it less utilized?

This is me making a big assumption, but from what I can tell, HSLa is much less utilized in the wild than RGBa is. If you have any ideas of why that is, please share in the comments. My theory is that HSL values are harder to get your hands on, as far as sampling from the screen or a Photoshop document.

I guess Photoshop uses Hue/Saturation/Brightness model instead. I honestly don’t quite understand the difference, but when I was first attempting this, this is the situation I got in.

Getting HSLa values

There is a little app for Mac called Colors from Matt Patenaude.

You just click the magnifying glass and then anywhere on the screen. It will fill in the color in the box. Then you can select the hsla option and copy it to your clipboard. There are preferences for tweaking the formatting just how you want it to. Works pretty well. My one gripe being that if you close the little floating window, it won’t reopen unless you quit and restart the app. Might be a Snow Leopard thing and the app hasn’t been updated in a while? I dunno.

If anyone has a favorite Windows or Linux tool for capturing color that has HSLa as a feature, let me know in the comments and I’ll update it here.

Browser Support

Firefox 3.0+, Safari 3+, Chrome 6+ (maybe older too?), Opera 10+ (maybe older too?)

As usual IE is left out of the party. Even version 8. I have a feeling version 9 will be supporting it but don’t have any proof yet. Anyone have it installed and can test? For IE, you can just declare a fallback color. IE 8 doesn’t support RGBa either, so just using a hex code is the best bet.

#some-element { 
  background-color: #e2ecf0; /* Fallback */
  background-color: hsla(190, 30%, 94%, 1);
  background-color: hsl(190 30% 94% / 1);
}

My Tool

As a demo of how easy it is to create color variations programatically, I created the HSLa Explorer. There are two slides on the page. The top one controls the hue value and the second one controls the opacity level. It takes those values and builds a grid of 100 color variations, where the saturation and lightness are varied in a matrix.

IE doesn’t like something in JavaScript (in the demo) for some reason, but no matter, IE doesn’t like HSLa anyway. Thanks to Matthieu Sieben for helping improve the demo.

More Tools


Yay for HSLa originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/yay-for-hsla/feed/ 41 6565