Closed LeaVerou closed 2 years ago
I think this will be the first time that CSS is using keywords to describe relative positions in the paint order. (If it isn't, we probably want to be consistent with the other cases.) I think over
and under
are a reasonable pair of keywords, but we probably want to think it through carefully, because there's a decent chance we'll want to use the same pair elsewhere. I think the main question is whether it's confusing to use them both for positions in paint order and positions in geometry.
(Perhaps the closest thing in CSS to this that I can think of is the Porter-Duff mode source-over
which Compositing and Blending Level 1 specifies for usage in canvas but not in CSS, and whose opposite (destination-over
) isn't useful for our purposes here. But this is consistent with that usage.)
most common case is to provide a fixed background color and multiple candidates for the foreground color
so what if it defaulted to over
and could be flipped with the under
keyword?
foo {
color: color-contrast(var(--_bg) vs var(--text-color-list));
}
bar {
background: color-contrast(under var(--_text) vs var(--brand-bg-list));
}
Could under
and over
be the joining word instead of vs
? So color-contrast(<color> [over | under] <color>#)
foo {
background: var(--brand-bg);
color: color-contrast(var(--brand-bg) under black white);
}
I'd prefer we keep the list comma-separated, which allows things like the ideas proposed in #7360.
Also note that the semantics of what you are proposing are flipped:
color-contrast(over white, red, black)
= white background, red/black foreground candidatescolor-contrast(white over red black)
= white foreground, red/black background candidatesThis means that for the most common use case (fixed background, foreground candidates) one would have to use under
rather than over
, which is suboptimal as it's a less understandable keyword.
I'm not sure what you mean by "under
is less understandable than over
".
Big +1 since this affects some contrast algorithm outcomes. Additional benefit: better code readability for DevX (for X over A B C
)
Have we considered separating such as X over / A B C
or even possibly X over / A, B, C
?
The CSS Working Group just discussed color-contrast should distinguish fg from bg
.
I'm not sure what you mean by "
under
is less understandable thanover
".
I think it's more common to speak of "colorX over colorY" than "colorX under colorY", so I'd have to do a double take on the latter. At first I might think it's talking about alpha blending or something, not foreground vs background.
The CSS Working Group just discussed syntax
, and agreed to the following:
RESOLVED: Keywords undefined
RESOLVED: Whatever keywords for foreground/background whatever they are are required
RESOLVED: No keyword ahead of algorithm list
More syntax ideas:
contrast-color(foreground red using wcag2(AA) vs blue, white, yellow)
contrast-color(foreground red using wcag2(AA) / blue, white, yellow)
(to make it more clear that it’s not 4 comma-separated items but something measured against 3 items)It’s a lot of words, but – to me – it’s very readable.
I really like the longer and more explicit syntaxes @bramus is suggesting, eg with ‘foreground’, ‘using’ and ‘vs’.
They make it very clear what's happening, which seems very helpful to developers encountering this syntax in stylesheets. It may also be easier to remember for developers wanting to use the syntax themselves (rather than just encountering it).
I like the verbose syntax a bit more too, however it immediately raises questions about how wcag3 options would be specified, because at that point it’s no longer just a single contrast value. Would you have to define your target contrast explicitly? Eg color-contrast( foreground red using wcag3(75) vs blue yellow white)
. The value needed to target is tied to font size and weight, so it seems this approach could quickly lose its utility (ie you’re less likely to type the wrong wcag2 value than you are a wcag3 value).
Before I go too far on this has there already been that consideration?
These algorithm-specific parameters could be extra arguments to the function, e.g wcag3(75, font-size, weight)
Great! Although I would guess in that example the first argument would be the level, not the desired contrast value: color-contrast( white using wcag3(silver, font-weight, font-size) vs yellow red blue)
. That is, assuming levels will be included (currently bronze & silver it appears)?
Coming here from Una's tweet and as specified in my comment I fear using over -some-color-
could be confusing. Confusing in the same way @media (min-width: 576px) {...}
can be. Sure it's not much of a hurdle to get over, but it's enough for me, even after about 7 years, to still have to think about it and double/triple/quadruple check every single time.
Other than that, I definitely prefer explicitness and easily understandable at first glance. Therefore, I can't +1 Bramus' suggestions enough. It does not to be the exact syntax suggested but the general direction of it.
Another +1 goes to Bramus for suggessing /
instead of vs
as separator. Bringing it on par with other color related functions and is more intuitive in my opinion.
With all that said, my vote is going for color-contrast(foreground red using wcag2(AA, other, args, if, necessary) / blue, white, yellow)
as of now. (a comma could also be used in place of using
here)
I am going to briefly list some ideas relating to syntax, scope, and automation first. The remainder of this post is the supporting discussion.
Suggested parameter order:
color-contrast(<target contrast> <fg/bg ID (bg default)> <color to test (default where invoked)> / <array of colors (default grey per contrast)>);
Parameter | Default | Optional Values | Example |
---|---|---|---|
target contrast | NA | contrast ID and target value | lc60 or wc4.5 |
fg/bg ID | bg or invoked | fg or bg | fg |
color to test | where invoked | any CSS color or color keyword | currentColor |
array of colors | grey per contrast | comma sep. list of CSS colors | #234, #abc |
Return fallback | grey/black/white | returns grey at contrast or max of black/white as fallback |
"Where invoked" means that if color-contrast()
is invoked in the background-color:
property, the color to test defaults to currentColor
, as we are returning the background color. Conversely, if we invoke as:
p { color: color-contrast(lc60); }
--myBackgroundColor: color-contrast(wc7 fg #e6e0dd / #123, #abc); }
In the first, simplest version, the algo is APCA (IDed by lc prefix) the color tested is the calculated background color, and the color returned is a grey that is Lc60 against the calculated bg color.
In the second example, because a variable is used, there is no invoked assumed use case, and either fg or bg should be specified (bg is default). The use of a slash as the divider between the first parameters and the color array/list permits unambiguous elimination of a specified color to test, which then defaults to the calculated background when invoked from the appropriate property type:
p { color: color-contrast(lc60 / #123, #abc, blue, yellow); }
Here the color to test is the default of the calculated background color in the p element, returning the text color from the list, because it was invoked from the color:
property.
If none of the colors calculate to Lc60, then a fall back is returned.
The fall back is probably either the best of white or black, OR more preferably, an achromatic grey when that can satisfy the specified target contrast.
Comments relating to the underlying reasons for the need (or not) to define foreground (typically high spatial frequency stimuli) vs background. (And not to mention vs the larger proximal field and other rabbit holes to explore.)
@LeaVerou said:
...provide a fixed background color and multiple candidates for the foreground color, we think the reverse should be possible as well... We do not think there are enough use cases that warrant the complexity of providing multiple candidates for both.
INDEED.
In terms of good guidance for minimum text contrast, there is not enough contrast range in an sRGB display to have both Lc -60 Lc and +60 even with a contrast color at the dead center of the range. The max is approximately Lc 53 or -54 ish (i.e. essentially half of the 106 or -108 range). The implication is that for fluent text, the polarity needed will be in one direction--if the fixed color is known.
BUT: if the fixed color is something like currentColor
or var(--someCalculatedColor)
then it is not possible to definitively know which polarity will result in the best contrast. And of course, this is the useful need for the color-contrast()
function, ya?
The contrast models that are polarity sensitive are also spatial frequency sensitive. There is good reason for this: polarity sensitivity directly relates to high spatial frequency stimuli. So really, when we are talking about foreground, background and which is lighter or darker (polarity) we are talking about a stimuli (foreground) that is high enough in spatial frequency (i.e. small and thin) that it is not subject to "contrast constancy".
At least for APCA, it is not "what is over" or "under". It is specifically:
Is the text lighter or darker than the background.
This is the property that is most important: which is lighter in the final render, the text/line or the BG.
APCA math naturally returns a positive number for dark text on a light BG, and returns a negative number for light text on a darker BG. The APCA guidelines also permit the use of an identifying string instead of a signed number. So, if using an absolute Lc value, the polarity should be notated as:
Lc +
= dark text on white = BoW = light mode = positive or normal polarity.
Lc -
= light text on black = WoB = dark mode = negative or reverse polarity.
The next question is, does color-contrast() need to have the color it is returning specified for use? Maybe, but possibly not, here are use cases:
In theory, when applied directly to certain CSS color properties, the "use" of the color to be returned is known:
section { background-color: color-contrast(currentColor, Lc60, <array of colors>); }
p { color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
div { border-color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
button { outline-color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
In the first example, since it is returning a background color, the first color is assumed to be either text, icon, border, or outline.
In the next three examples, the first color is assumed to be the background, as text, border, or outline are the "high spatial frequency foreground".
But a significant problem arises with the use of variables, wherein the returned color is not implicitly known:
:root {
--sectionBG: color-contrast(fg: currentColor, Lc60, <array of colors>);
--pTextColor: color-contrast(bg: #e6e0dd, Lc60, <array of colors>);
--divBorderColor: color-contrast(bg: #e6e0dd, Lc60, <array of colors>);
--buttonOutline: color-contrast(bg: #124433, Lc60, <array of colors>);
}
A related syntax issue: arguably, the most important value is the amount of contrast and the algorithm used. Here is a use case where only the contrast value is used:
p { color: color-contrast(Lc60); }
Perhaps this is too much for a CSS function to handle? But here, as color-contrast()
is in the color:
property, then we know we are to return the text color, and therefore want a text color that is a grey that is Lc60 relative to the current background-color (default).
Let's next consider algorithms. It should not be needed to specify the algorithm separately from the contrast value, as the contrast value can easily contain an identifier for a given algorithm.
Algorithm | ID | Example Use |
---|---|---|
∆L* | lab | lab60 |
APCA | lc | lc60 |
WCAG2 | wc | wc4.5 |
BridgePCA | bc | bc4.5 |
Michelson | % | 60% |
RMS | rms | rms60 |
While we have the keyword currentColor
which could be put to good use with color-contrast()
there is use for new keywords currentBackgroundColor
, currentBorderColor
, and possibly currentOutlineColor
.
As mentioned above, "default" behavior seems ideal to match per the property from where invoked.
Property Invoked From | Default Test Color | Determined From |
---|---|---|
background-color: | currentColor | current/calculated color |
color: | currentBackgroundColor | calculated background color |
border-color: | currentBackgroundColor | calculated background color |
--variable: | NO DEFAULT/MUST SPECIFY | Specified by ID |
box-shadow: | currentBorderColor OR currentBackgroundColor if no border | calculated border or background color |
text-shadow: | currentColor | current/calculated color |
More complete appearance models have multiple color inputs. For the sake of simplicity, and also for "familiar use case", the base APCA works with only a pair: the high spatial frequency foreground, and the low spatial background (which guidelines further indicate should have ~1em padding around text, if the larger background is significantly different).
SAPC and SACAM have additional inputs, and it's not out of the question for APCA to have either a proximal field (larger encompassing bg) or a "peripheral anchoring" input.
This might be defined as "foreground(text)", "local background", "larger background".
I mention this as in an automated context, the larger proximal may gain importance. Particular in the use case of text -> button -> background. I mention this as there may in the future be a need to consider three-way colors for a more complete automated solution.
In the current pair-wise APCA, the assumption is a proximal field and ambient that is at a common and "worst case" level, which in practical terms means a proximal field of between about #dddddd
and #ffffff
. As you can see in the following graph, a bright proximal field pushes "light mode" and "dark mode" closest together.
Also, "light mode" (dark text on a light BG) is most influenced by changes in the proximal field, at least as far as center contrast in concerned. But also, dark proximal fields "improve" light mode contrasts, but has minimal effect on dark mode contrasts.
As such, assuming a bright proximal field is both reasonable and useful.
Footnotes:
Thank you for reading,
Andy
@Myndex I like what you're thinking in terms of arguments and possibly identifying color by its context in CSS (although I'm equally unsure if that's possible).
I would not recommend multiple contrast formulas, however. The purpose should assist in WCAG compliance, therefore I would expect only their approved formulas. Adding any of these other ones suggests they are acceptable alternatives.
Also I am curious if you have more information on the chart you've shared. That would likely be better suited in a different thread, but in this context are you illustrating the visible perceived contrast thresholds at various lightnesses? Ie, charting thresholds similarly to as you would in the CSF except using proximal luminance/lightness rather than spatial frequency (assuming frequency is fixed)? Although even that doesn't seem right as it's charting white-on-black and black-on-white, so clarification would be helpful if you have a link to more information 😇
Hey Nate @NateBaldwinDesign
The multiple contrast formulas for color-contrast()
is not my idea, it is listed elsewhere including at the top of the thread, and often indicated as a separate parameter—my response is simply that if multiple algorithms are available, that the context of the contrast value and an identifier is probably the best practice.
The chart is middle contrast between black and white with different proximal fields, and different configurations of the contrast matching experiment, ranging from black to white in increments of L* 25 (in this example). For the "more info" this is part of a paper-in-progress. More to come there!
The purpose should assist in WCAG compliance,
No, the primary purpose is to ensure fluent readability by setting an appropriate level of lightness contrast.
Using the WCAG 2.x formula is one way to do that, but it has well documented problems and other formulae are better.
therefore I would expect only their approved formulas. Adding any of these other ones suggests they are acceptable alternatives.
Mandating solely WCAG 2.x suggests that it is an acceptable formula; for dark mode, and for many color combinations, it is not.
I would not recommend multiple contrast formulas, however.
That is
the context of the contrast value and an identifier is probably the best practice.
@Myndex got it, that makes sense. I read too much into the presence of Delta-L, Michelson, and RMS. @svgeesus that's primarily the reason for my statement on not recommending multiple contrast formulas. I should be more clear on what I'm trying to say! Essentially, as @Myndex mentions in #7357, Delta-E L is similar to relative luminance and may have similar pitfalls. What I want to be cautious about is what constitutes a supported formula for color-contrast()
. If it's a grab-bag of available options, there could be more harm than good (Hick's law). For many designers & developers who are not familiar or interested in learning the nuance of contrast perception and measurement, it can become a hurdle. If a desired objective for having many different formulas is for testing to find a better contrast formula, I don't believe incorporation into a core CSS feature is the right way to go.
No, the primary purpose is to ensure fluent readability by setting an appropriate level of lightness contrast.
Yes; I am not in disagreement with this. But considering this is a w3c specification, and WCAG is the w3c's standards initiative for making the web more accessible (which includes readability of text), I would expect parity in recommendations and formula. This would only be an issue if additional formulas beyond APCA are being considered for this feature.
Mandating solely WCAG 2.x suggests that it is an acceptable formula; for dark mode, and for many color combinations, it is not.
I'm not recommending mandating 2.x alone. This is a misunderstanding from poor phrasing above -- I completely agree with supporting WCAG 2.x relative luminance and WCAG 3 APCA formulas. I'm aware of WCAG 2.x formulas flaws.
Thank you for sharing the other thread, I realize my comments are more relative to that issue.
Delta-E L* is similar to relative luminance and may have similar pitfalls.
I'm interested in this one because it is what Google Material Design uses (HCT Tone is the same as CIE L). Also because L is perceptually uniform (but as @Myndex has pointed out, for large blocks of color not for higher-frequency items like text). However, it seems to give very similar results to WCAG 2.1, except for a few mid-tone colors. To see this, on the black or white app:
There are not many differences.
If a desired objective for having many different formulas is for testing to find a better contrast formula, I don't believe incorporation into a core CSS feature is the right way to go.
This is why I implemented several of them in color.js, so people can experiment. So far only APCA is giving good results.
SAPC and SACAM have additional inputs, and it's not out of the question for APCA to have either a proximal field (larger encompassing bg) or a "peripheral anchoring" input.
Could you point to the formulae for these, so I can see how a proximal field is used to affect the calculations?
Hi Chris @svgeesus
Could you point to the formulae for these, so I can see how a proximal field is used to affect the calculations?
I'd characterize this is "not quite ready for prime time" if only because I want a larger dataset to better define the interactions.
One approach (preferred at the moment) uses the field input to adjust the power curve exponents for the given polarity set. But then we also need to determine the contrast between the local background and the larger field background, i.e. the button against the field, in addition to the text inside the button.
So, we make the assumption then that the localBG to fieldBG is a low spatial frequency relationship (sharp edge not withstanding in the interest of simplicity).
Therefore: localBG -> totalBG is calculated with smaller exponents (i.e. closer to 0.425 ish) due to the lower contrast center (and lower contrast constancy) for the assumed lower SF, and then the text -> localBG is calculated with dynamic exponents, and here for BoW "light mode" polarity, the exponents are lowest when the fieldBG is near about 20 Y but for WoB "dark mode", the text -> localBG exponents are highest when the field is near 20 Y. (See that chart I posted earlier).
Also, light mode BoW is affected most by the field changes, and dark mode is most immune, at least at higher contrasts. None of these shifts are linear or even in the same direction as the field changes. Hence I am working to collect more data from more users before dialing this all in. And also, this means that right now I'm working with LUTS and I don't have a "pure curve' solution, though that is the eventual goal.
A related issue is how the fieldBG affects the calculation or consideration for a maximum contrast (i.e. the point where halation becomes a problem). As darker fields tend to lower the halation point's luminance and therefore lowers the max contrast value.
And all of this is in consideration of "keeping the number of layers of this onion to a most reasonable level" and "minimal hand waving at the Cheshire Cat as one plummets past that smirking feline down this rabbit hole to China" or words to that effect.
Tangentially, I am approaching a simple curve solution relating to spatial frequency effects for font weight and size, at least for Latin-based stimuli and for abstract dataviz elements.
As we move towards a
color-contrast()
function that is more flexible and allows specifying the algorithm used (see #7356), we run into the issue that many (most?) contrast algorithms are not commutative operations; it makes a difference which of the two colors is background and which one is foreground. Currently,color-contrast()
does not distinguish between the two, but we think the syntax should make it clear which one is which.While the most common case is to provide a fixed background color and multiple candidates for the foreground color, we think the reverse should be possible as well (fixed foreground, variable background). We do not think there are enough use cases that warrant the complexity of providing multiple candidates for both.
@fantasai suggested the following syntax, which also addresses the syntax concerns raised in #7354:
color-contrast([over | under] <color>, <color>#)
(target level omitted from this grammar for simplicity)(Issue filed following breakout discussions between @svgeesus, @fantasai, @argyleink and myself)