Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@una
Last active March 11, 2020 13:15
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save una/edcfa0d3600e0b89b2ebf266bf549721 to your computer and use it in GitHub Desktop.
Save una/edcfa0d3600e0b89b2ebf266bf549721 to your computer and use it in GitHub Desktop.
CSS Color Functions

CSS Color Functions

Web developers and design systems developers often use functions to design components. With the increasing usage of design systems that support multiple platforms, and increased capability of Dark Mode in UI, this becomes even more useful to not need to manually set color, and to instead have a single source from which layouts are calculated. Currently Sass, calc() on HSL values, or PostCSS is used to do this.

Examples:

  • Components with color variations based on calculations from a parent. (i.e. Button with outline that uses the primary button color to adjust the size)
  • Theming - Palletes based on a color or set of colors for themes. Especially when a single base system is used with multiple themes
  • Uniformity among transformations between components with different primary colors

Proposal

  • 3 functions: adjust-color, mix-color, and contrast-color
  • LCH is the default color space. You can specify transformations in another color space (ie. HSL lightness instead of the default LCH lightness - lightness(red, lch)). If the argument is non-compliant with the color space (i.e. lightness(red, cmyk)), the transformation will be clipped to CMYK after an LCH transformation.
  • In this example, like filter, > 100% or 1 = positive change (i.e. lighter), and < 100% or 1 = negative (i.e. makes it darker) and/or we can do negative and positive values based on 0 (- 30% || + 30%)

adjust-color

  • Adjusts color via:
    • lightness
    • chroma
    • hue
  • adjust-color(<color>, <colorFunctions(<amount>,<optional color space>)>)
  • ex. adjust-color(red, lightness(130%) chroma(0.4));
  • ex. adjust-color(red, lightness(130%, hsl) chroma(0.4, lch)); ← with color space argument

mix-color

  • Mix two colors by an specified ammount of the first value. See examples:
  • mix-color(<color1>, <color2>, <colorFunctions(<amount>,<optional color space>)>)
  • ex. mix-color(red, yellow, lightness(30%) chroma(0.8)); ← 30% of red lightness and 70% of yellow lightness, 80% of red alpha and 20% of yellow alpha
  • ex. mix-color(red, yellow, lightness(30%, rgb) chroma(0.8, lch)); ← specifying color spaces

contrast-color

  • Determine which of the list of colors (second input, space-separated list of values) has the highest contrast with the background (first input)
  • contrast-color(<bgcolor || currentBackground>, <color>{2,}) ← which of these (2+) colors is the most contrasted
@svgeesus
Copy link

svgeesus commented Feb 5, 2020

@lunelson wrote:

Who is out there relying on the muddy or improperly lightened color adjustments in other spaces...

I'd like to point out that the reputation for incorrect mixing and adjustment in other spaces (RGB, HSL) stems in large part from failure to gamma-convert the input sRGB color space, I see this mistake again and again, especially in current techniques which as mentioned are implemented in Sass, PostCSS etc.—since for example in Sass we lack the required math functions. RGB and HSL spaces can both give good results if transformation is done in linear space however.

I agree that directly calculating on gamma-corrected values is depressingly common. CSS Color 4 does this correctly, and even gives sample code for the color conversions. CSS Color 5 depends on CSS Color 4 and so, also does it correctly.

Unfortunately, Compositing does it the wrong way, mainly for compatibility with Photoshop blending modes which work on gamma-corrected RGB colors (and even use the NTSC values for luminance calculations!)

For HSL, are you applying the HSL equations to the un-gamma-corrected RGB values?

@svgeesus
Copy link

svgeesus commented Feb 5, 2020

@una wrote:

The reasoning for a color space option (to adjust in other color spaces) is to help people transition their existing systems for accurate results. Currently, many are using either Sass/PostCSS functions (which are RGB or HSL based) or manual HSL calculations and matching their current color algorithms would make adoption easier and more accurate. An author may want to use CMYK if they are applying design for print.

Good point, I need to add a pair of examples where one does calculations in LCH and the other in HSL, to show the difference.

@xi
Copy link

xi commented Mar 5, 2020

Nice to see some of my biggest heroes when it comes to CSS and color in one thread!

A while back I wrote the library sass-planifolia which covers many of these ideas, so I may have some relevant experience:

mix-color

In hue-based color spaces you have to consider chroma when mixing. Black typically has a hue of 0, so a naive mix of black and blue ends up as grayish-purple. I use something like this to calculate the mixed hue:

hue = (hue1 * chroma1 * amount + hue2 *chroma2 * (1 - amount)) / (chroma1 * amount + chroma2 * (1 - amount))

Not sure how we'd handle semi-transparent colors, since the contrast in that case is a range, so how do we order overlapping ranges?

For picking a color from a list I think that using worst case is the best way to go. So if you can pick between a color with contrast 4 and semi-transparent color with contrast 3-6 you pick the former. (Transparent backgrounds are often used on top of images, so it is very likely that the worst case will occur)

adjust given color to meet a particular ratio

The simple way to do this is guessing a lower and higher bound for lightness and then doing a binary search. That's what I do in the library. Not exactly efficient, but also not that bad (O(log(n))).

I also chose to reduce chroma instead of clipping. I think people expect to preserve hue more than chroma.

  • min contrast to meet a particular ratio (from list)
  • adjust given color to meet a particular ratio

I imagine the latter may open up too much complexity - but I’d consider the former a primary use-case.

I always found these functions great in theory, but I have never found a cases where I could use them in an actual project. The lack of libraries that implement this (AFAIK mine is not really being used by anyone, and I don't know any other that is) could also be considered a hint that there is not much demand for this.

Most designers I have worked with like to keep control about the colors that are used. A function like the limited color-contrast() from the original proposal is perfect for that. So as much as I would like to have these extended functions personally, I think concentrating on the limited scope from the original proposal is the better option.

In summary: I am really excited about this proposal and would love to see this in browsers. I especially like the simplicity and would recommend to avoid pushing in any more features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment