@mirisuzanne wrote
I like the basic thrust, but working on design systems, I often need more control than max-contrast from a list. I鈥檇 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鈥檇 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)
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.
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.
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.
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.
@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.
I like the proposal here, where the ratio provides a minimum contrast 鈥撀燼nd selects the first passing value in the list. If none pass that ratio, I see several options:
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 鈥撀爓here 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.
@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?
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鈥β爓ouldn't want a blue to be adjusted to be a brown or a green, for example.
@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?
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.
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)
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.
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
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.
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 馃憤
Most helpful comment
I like the proposal here, where the ratio provides a minimum contrast 鈥撀燼nd selects the first passing value in the list. If none pass that ratio, I see several options:
black
orwhite
, 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 鈥撀爓here 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.