w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.42k stars 652 forks source link

[css-color-5] add target contrast ratio to color-contrast? #4749

Closed svgeesus closed 3 years ago

svgeesus commented 4 years ago

@mirisuzanne wrote

I like the basic thrust, but working on design systems, I often need more control than max-contrast from a list. I’d like to see these uses considered as well:

* 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.

Currently, the color-contrast() function takes two arguments: a color, and a list of colors.

This requirement could be addressed by adding a third parameter, which is a target contrast ratio. Instead of comparing the whole list, comparisons stop once the target contrast ratio has been reached or exceeded.

This assumes that the list is in order of desirability, but the resolution of #4732 already implies this (if two colors in the list have the same contrast, the earlier in the list wins; this implies the stylesheet author put them in order of desirability).

color-contrast() = color-contrast( <color>  <color>#  number?)

The number is optional and if omitted, the whole list is searched to find the highest contrast ratio.

So for example

color-contrast(tan var(--blue1), var(--blue2), var(--blue3), var(--blue4) 4.5)
svgeesus commented 4 years ago

Oh and we would need to define what happens if none of the colors in the list meet the target contrast ratio

One option would be to return whichever of white #FFF or black #000 gives the highest contrast ratio with the first parameter color.

devmattrick commented 4 years ago

I really like the latter idea (to me it feels more natural than the former), although I really have no idea if there's a mathematical way to derive an acceptable color while minimizing changes to the perceived color (I do imagine there is, though).

In my view, the whole point of a color-contrast function, especially when supplied a given minimum contrast ratio is to calculate a color that meets the minimum contrast requirement instead of "the best of what we have," which may not meet that contrast requirement. The former method may be confusing as well due to the way how color-contrast currently works, where it selects the best of a list of colors, but when supplied a minimum contrast ratio, it selects the worst of a list of colors, in a sense.

To me, it feels much more intuitive always selecting the best possible color from the list and then adjusting it with minimal impact to the perceived color to meet the given contrast ratio.

una commented 4 years ago

What about if you want to pass a specific ratio (i.e. the result would be the color closest to that ratio) instead of just min and max?

The closest value to the ratio would be selected (this would be the most likely case)

If the ratio is identical between 2+ numbers, the result would be the first in the list.

svgeesus commented 4 years ago

where it selects the best of a list of colors, but when supplied a minimum contrast ratio, it selects the worst of a list of colors, in a sense

No, perhaps I explained it poorly. It selects the first one that meets the specified contrast ratio (or none, if none of them meet it). It doesn't select the one with the worst contrast.

devmattrick commented 4 years ago

@una I believe that still runs contrary to the current function of color-contrast, which is to get the best possible color from a set of colors. This is definitely just personal opinion, but I find that "get a color with an acceptable contrast ratio" would be much more useful than "get a color that is closest to this ratio, even if it fails to pass this ratio." Its definitely worth hearing more people's opinions on this, though.

@svgeesus I may have explained it poorly too, lol. The issue I have with it is say if multiple colors in the set have a greater ratio than the target, it will select the worst of the bunch. For example, if I have color-contrast(#000 #FFF, #CCC, #333 13.0), it will select #CCC, despite it being the worst of the two that pass the ratio requirement. Whereas the color-contrast function without a ratio would return #FFF.

In the case where none of the provided colors meet the required ratio, how would designers implement a fallback to this? They could potentially just "chain" the same color-contast function (without a ratio specified), but then this would 1) not provide an acceptable ratio and 2) be much less ergonomic than using a single function, as it would require them to do something like:

Edit: Apologies, I misunderstood your second comment. This is definitely better than what I thought was how failing checks were handled, but this behavior might be unexpected.

mirisuzanne commented 4 years ago

I like the proposal here, where the ratio provides a minimum contrast – and selects the first passing value in the list. If none pass that ratio, I see several options:

  1. Return the maximum contrasting color from the list, as though no ratio was given. This matches existing default behavior for the function, and leaves control with the author for better and worse.
  2. Return the better contrast of black or white, as proposed above. This better matches the intent of ensuring an accessible ratio.

The idea of returning "closest ratio" is interesting. In some ways a nod to the idea that "contrast" is not always only about accessibility or text/background, but might be useful for other design goals – where you don't need to "pass" a particular value, but are more interested in creating a particular effect. Still, I think being able to order your preferences in a list, and select between min-or-max, would work pretty well to support those use-cases.

mirisuzanne commented 4 years ago

@devmattrick I don't understand the implication that higher contrast is always "better". If that were the case, I would always want to select between black and white. The goal here is to allow for more careful selection of colors that pass a particular contrast ratio, without assuming we always want the highest contrast possible.

Thinking about the highest-possible use-case, though - would there be value in a highly-simplified default where color-contrast(tan) acts as color-contrast(tan white, black)? If no list is given, use a default list of black and white?

jhogue commented 4 years ago

FWIW I created a Sass function called a11y-color() that takes a color to change, a color to keep the same, a WCAG Level (AA or AAA), a font size, and whether or not a font is bold to calculate and change the first color (color to change) to a color that passes the supplied WCAG Level.

It uses the formula that WCAG 2.1 supplied in their guidelines, which is problematic for some color combinations. I am hoping that the newer lch() color model and formulas will help.

I like the promise of the proposed color-contrast() function but, for selfish reasons, would prefer that it does something closer to what I was doing with Sass. Adjust one color in a pair to pass a threshold. A list of colors is ok, but seems messy. In order to have a list of colors to choose from, the designer should have provided and accounted for all possible color combinations. My function aims to account for the edge cases where a designer may not have created a passing color pair. It feels like color-contrast() should do the same and help account for edge cases where a particular combination has not been tested and the brand color palette has not been adjusted. Importantly as well, any adjustments should maintain the Hue of a color so as not to change it dos drastically… wouldn't want a blue to be adjusted to be a brown or a green, for example.

svgeesus commented 4 years ago

@jhogue what you propose is useful, but as an addition to (rather than a replacement for) the current function.

It uses the formula that WCAG 2.1 supplied in their guidelines, which is problematic for some color combinations. I am hoping that the newer lch() color model and formulas will help.

You mean this discussion?

jhogue commented 4 years ago

Yes indeed. Very in-depth and interesting use case, but I was also referring to https://www.w3.org/TR/2020/WD-css-color-5-20200303/#relative-LCH specifically. It was the first I had heard of LCH to be used in CSS.

NateBaldwinDesign commented 4 years ago

Hi there -- new to this conversation. I have some thoughts on all of this. I would expect a color-contrast() function to simply run a pass/fail test of two colors, however I think the simple behavior of passing a list of possible colors with a target ratio, and returning the first color to meet that ratio (or white/black if none do). However, in terms of adjusting the color to meet the target ratio, you're going to encounter problems that can't be solved by formulas and color spaces alone.

For example, regarding this statement

I am hoping that the newer lch() color model and formulas will help

LCh color model is not an end-all solution. There tends to be an assumption that different color models will automatically solve all of our human-perception-of-color issues, however it's simply not the case. There are continual advancements in color science in order to make models that are even more perceptually accurate (CAM02 and CAM16, developed in 2002 and 2016 respectively -- quite a bit more modern than LCh; a cylindrical adaptation of Lab which was defined in 1976). Even those don't solve all problems.

There are also a lot of factors that go into selections of color, including how the color should adjust as it gets brighter or darker. There are aesthetic choices that can't be resolved by colorspace -- such as making a yellow color become more orangish as it gets darker. And to that effect, defining how much and at what rate the color gains/loses saturation. On top of that, default interpolation is linear, which does not always accurately reflect how designers would expect a color to change as it approaches black/white (darkest/lightest possible color value). For that, designers may hope to have finer control, such as a smoothing function to ensure the color interpolates along a curve.

This problem is so complex I've made an (arguably just as complex) tool for designers and engineers to do exactly that-- generate colors based on a target contrast ratio (https://leonardocolor.io/). The engineering experience aims for the same thing it looks like being discussed here -- a single function call where you enter a color, a target ratio, and it spits out an appropriate color. However, because of the nuanced aesthetic choices that need to be controlled, this type of function also needs additional parameters:

generateContrastColors({
  // specific target colors provide control over how color changes:
  colorKeys: ["FFFF00", "553300"],  
  // different color spaces may provide more desirable outputs:
  colorspace: 'RGB',
  // default interpolation is linear, however a smooth option may be better:
  smooth: true,
  // background color is needed as it takes 2 to calculate contrast:
  base: '#ffffff',
  // perhaps you want more than one color output from this scale:
  ratios: [3, 4.5]
});

It would be fantastic if in the future there was a CSS color function that helped with all this, however it's important to not overlook the complexity of color, color selection, and adherence to aesthetics while manipulating a color to meet a target contrast ratio. All of that would come with the need for designer-facing tools to configure the parameters for engineers to use in the product. But until then, I think it is better suited to approach either of the simpler solutions above (simply calculate contrast, or return a color from a list of possible colors that matches the target)

svgeesus commented 4 years ago

Hi @NateBaldwinDesign thanks for the helpful commentary and also the link to leonardocolor which we (the editors) have been playing with of late.

In terms of what the proposed color-contrast() does, if the current specification gives the impression that the function replaces careful design and user testing, we would certainly want to reword to avoid suggesting that conclusion. It is just a utility function; intended to give some help to stylesheet authors beyond the current situation, which is to not consider contrast at all. For a static color scheme, of course, contrast can be evaluated with human subjects or a site analysis tool. As we move into more dynamic, thematic-based color schemes, such testing becomes more complex and lengthy, so we felt that giving CSS itself the ability to perform some of that testing would be helpful.

It's interesting that you mention the CIECAM color appearance models, and it is reasonable to ask why CSS Color 4 and 5 are defined in terms of colorimetry (Lab and LCH) instead of color appearance. The reason that color appearance (which was considered) could not be used in CSS is because of the inherent nature of CSS. Style rules from various origins (author, reader, user agent) are combined via specificity and cascading to yield an eventual result. Thus, all colors are specified at a very granular level on individual elements in the document tree. There is thus no notion of the overall visual field or surroundings in which colors will take on an appearance. Certain aspects of that (the background color or image for a specific element, the colors of nearby elements) could in theory be tractable to analysis by a CSS processor. Others (the colors of other windows that are visible in addition to the browser window) are not available (and must not be, for security and privacy reasons); the overall room luminance, the current white point and the degree of user adaptation to that white point are unknown to a CSS processor and thus cannot be used as input to a color appearance model.

I had already noticed that your leonardocolor tool offers CIECAM02 as an option, but I was unable to find how to indicate any of the required parameters (such as adapting field luminance, chromatic induction factor, lightness contrast factor or the factor for the degree of adaptation) once it was selected. How do you account for these in your tool?

Your point about better-than-linear interpolations and the need for easing or smoothing functions is well made.

faceless2 commented 4 years ago

That's a fantastic looking tool Nate.

I had the same question as svgeesus actually. As the CIECAM02 model appears to be built on D3 color, the answer is here: https://github.com/connorgr/d3-cam02

Both Jab and JCh assume average viewing conditions for the purposes of computing CAM02 color.

and here: https://github.com/connorgr/d3-cam02/blob/master/src/cam02.js#L105

NateBaldwinDesign commented 4 years ago

Thank you for sharing the link Mike, that's correct.

Quick caveat, sorry if this is not the appropriate location for this discussion

CIECAM02 is a perceptual adaptation of CIELAB colorspace, and the d3-module allows us to leverage it as an optional color space for interpolation. Some of the interest in providing CAM02 has been inspired by the creation of Viridis, Magma, Inferno, and Plasma (from CRAN: https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html). These scales were created by drawing a bezier curve within a confined plane of CAM02 space in order to leverage its perceptual uniformity. There's a great walkthrough they give here: https://www.youtube.com/watch?list=PLYx7XA2nY5Gcpabmu61kKcToLz0FapmHu&v=xAoljeRJ3lU.

When evaluating these scales and seeing the smooth transitions between hue and chroma, it makes sense (from a designers perspective) that these follow a smooth curve. However, in other color spaces (including Lab/LCh), a smooth curve does not produce a perceptually smooth transition. Albeit this is case-by-case, and certain cases are not as clearly different.

For example, here's a comparison of a smooth (bezier) interpolation between three key (sample) colors (#FDE725, #218F8D, #440154). One is a smooth transition in LCh, the other is in CAM02. In this particular case, CAM02 provides a much smoother transition between colors. Mapping these interpolation paths also made it clear how irregular the interpolation path is in LCh. image

Why am I going on this tangent? First, many of the same problems that a user would encounter when creating a sequential scale for data visualization are shared by users determining how to adjust tints and shades of a color in order to lighten/darken for a particular contrast output.

Secondly, LCh will not solve all your problems. :-)

Anyway, all this is to shed light on the problem I've already expressed, so I hope this is not too redundant or unnecessary. The utility expressed in the description of this issue seems to meet what I'd expect/hope for. Looking forward to seeing where this goes 👍

mirisuzanne commented 3 years ago

When I see people talk about the color-contrast() function, it often looks like this comment in another thread from @argyleink:

p {
  color: color-contrast(
    inherit(background-color) 
    vs 
    var(--brand-1), var(--brand-2), var(--brand-3), 
    white, black)
  ;
}

I like that example, but it also demonstrates this issue perfectly. Despite a list of three brand colors there, obviously the preferred values, the current function will always select black or white - guaranteed to have the highest contrast.

With browsers starting to implement color-contrast, I would really like to see this addressed.

argyleink commented 3 years ago

demonstrates this issue perfectly

happy to be of service 😅

I'd very much like to specify a target contrast. The "design goal" is usually to avoid usage of white/black; to stay within the tone of the brand color set instead and reserve usage of black and white to specific design cases. Generally, the "minimally viable/passing contrast color" is the one that design will want to use the most, and allowing us to specify which contrast target range we want, we can give up calculating/targeting these ratios on our own and let CSS "pick from the pool" we prepare as authors.

I feel this is pretty critical to the convenience and power of this color-contrast() function. Strong agree with @mirisuzanne here 👍🏻

svgeesus commented 3 years ago

Summing up this thread, there seems to have been some confusion between two very different options

  1. One color to keep, list of colors, optional target. Pick the first color from the list that hits the target. If no target given, pick the color with highest contrast.
  2. One color to keep, one color to modify, {target and other metadata like boldness, font size etc}. Modify the color in some way so it meets the criteria.

The first of these is the topic of the issue. It is a small adjustment to the existing color-contrast() function. I see @argyleink and @mirisuzanne in favor of this option.

The second is interesting, but is a totally new function and would need a concrete proposal. WCAG 2.1 contrast is also being radically changed in WCAG 3.

There has also been some great discussion about the color modification aspect of the second option, parts of which could also influence further development of color-adjust(), gradients, transitions and animations:

As @LeaVerou just observed when we were discussing this issue, I wish there was a way to fork (parts of) an issue into a new issue).

Getting back to option one though, @mirisuzanne I see your Agenda+ was this to suggest adopting the proposal? As there are no objections to it in this issue, I think we can just do it.

mirisuzanne commented 3 years ago

@svgeesus yes, I just wanted to push us towards adopting the proposal. If we don't need a resolution for that, I'm happy with you making the change.

LeaVerou commented 3 years ago

What about to <number> instead of just a raw <number> randomly in the function which makes it hard to understand?

svgeesus commented 3 years ago

If no color in the list meets the target, what do we get? We need to return something, so whichever of white or black I guess?

LeaVerou commented 3 years ago

If no color in the list meets the target, what do we get? We need to return something, so whichever of white or black I guess?

I like that. Saves people from having to specify them explicitly like @argyleink did. People still can specify them if they have a preferred one (e.g. white over black).

mirisuzanne commented 3 years ago

Yeah, it's either that or "closest-color", and I agree black/white is the better default:

  1. For the important accessibility use-cases, it provides the best usability backstop
  2. If the author wants to avoid white/black, it provides a clear visual "failure" to provide appropriate colors
  3. For non-a11y use-cases, the author can always lower the ratio or add more options
svgeesus commented 3 years ago

Worked example for the spec

image