Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-color-6] color-contrast() should distinguish foreground and background #7359

Closed
LeaVerou opened this issue Jun 14, 2022 · 23 comments
Closed
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. Closed Accepted by CSSWG Resolution css-color-6

Comments

@LeaVerou
Copy link
Member

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)

@dbaron
Copy link
Member

dbaron commented Jun 15, 2022

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

@LeaVerou LeaVerou added the a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. label Jun 15, 2022
@svgeesus svgeesus removed the css-color-5 Color modification label Jun 15, 2022
@argyleink
Copy link
Contributor

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));
}

@JoshTumath
Copy link

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);
}

@LeaVerou
Copy link
Member Author

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 candidates
  • color-contrast(white over red black) = white foreground, red/black background candidates

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

@tabatkins
Copy link
Member

I'm not sure what you mean by "under is less understandable than over".

@svgeesus svgeesus changed the title [css-color] color-contrast() should distinguish foreground and background [css-color-6] color-contrast() should distinguish foreground and background Jun 22, 2022
@una
Copy link
Contributor

una commented Aug 2, 2022

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?

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed color-contrast should distinguish fg from bg.

The full IRC log of that discussion <TabAtkins> Subtopic: color-contrast should distinguish fg from bg
<TabAtkins> github: https://github.com//issues/7359
<TabAtkins> chris: Most of the algos, you give two colors, and they generally denote one of them is lighter and the other is darker
<dbaron> (when skimming the list of algorithms you referenced... I only saw APCA that cared which was fg/bg)
<TabAtkins> chris: At least APCA, and som eothers, requires you to say which is the text and which is bg, and if you swap you don't get the same number
<TabAtkins> chris: wcg doesn't care
<una> q+
<TabAtkins> fantasai: two proposals in the issue
<TabAtkins> fantasai: both of them are suggesting an over/under keyword
<TabAtkins> fantasai: one of them puts it as first arg color-contrast(over white, ...) meaning white is bg
<TabAtkins> fantasai: this follows same pattern as gradients
<TabAtkins> fantasai: downside is because arg is a color, it looks like part of the color list
<TabAtkins> fantasai: other suggestion was to use keyword as divider color-contrast(white under ...)
<fantasai> https://github.com//issues/7359#issuecomment-1158668669
<TabAtkins> fantasai: argument against was it felt a little backwards about which keyword you used
<castastrophe> q+
<TabAtkins> lea: the way the comma works, it tends to have higher precedence than just words
<TabAtkins> lea: so using a word to separate from a comma-seaprated list, it looks like you have one big first entry in the list
<Rossen_> ack una
<TabAtkins> lea: But that's a problem with the current syntax
<dbaron> +1 to lea about comma versus space precedence
<TabAtkins> una: have we considered a slash rather than comma? like `red over / <color-list>`
<TabAtkins> una: seems to make it easier to separate
<lea> q+
<Rossen_> ack castastrophe
<TabAtkins> castastrophe: i love that solution
<TabAtkins> castastrophe: like the natural language read of "color over"
<TabAtkins> castastrophe: +1 to slash
<Rossen_> ack dbaron
<Zakim> dbaron, you wanted to suggest []
<TabAtkins> dbaron: suggest bracketed list
<TabAtkins> dbaron: grid tracks do this
<bramus> q+
<chris> +1 to a slash or some other way of grouping the list
<TabAtkins> una: my first thought was bracket list
<astearns> so `red over [<color-list>]`
<TabAtkins> una: just think it's more of a convention to use the slash
<Rossen_> ack lea
<TabAtkins> lea: issue with nested function...
<TabAtkins> TabAtkins: not function, just brackets around the color list
<TabAtkins> lea: oh, that's different
<TabAtkins> lea: we have a precedent of using slashes to separate parts within comma list, in backgrounds, so using it here with opposite precedence
<bramus> q-
<fantasai> TabAtkins: That was going to be my objection as well
<chris> looks like an array: white over [red, blue, violet]
<TabAtkins> fantasai: suggest we bikeshed over lunch
<bkardell_> 👉 red, black 👈
<TabAtkins> <br dur="1h 10m">

@LeaVerou
Copy link
Member Author

LeaVerou commented Aug 2, 2022

I'm not sure what you mean by "under is less understandable than over".

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.

@css-meeting-bot
Copy link
Member

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
The full IRC log of that discussion <emilio> subtopic: syntax
<fantasai> github: https://github.com//issues/7359
<fantasai> Suggestion is color-contrast([over|under] <color> <contrast-algo>#, <color>#)
<fantasai> Suggestion is color-contrast([over|under] <color> <contrast-algo>+, <color>#)
<emilio> emilio: why a +
<emilio> fantasai: because they're space-separated
<emilio> una: example w/ colors?
<fantasai> color-contrast(over white wcag2(AA), blue, red, green)
<emilio> astearns: which of blue, red, green looks best over white
<emilio> una: I read white as foreground
<emilio> TabAtkins: proposition first because of gradients
<emilio> una: here I expect location + color value
<emilio> fantasai: we intended to read not as a param but a proposition
<emilio> bramus: why not background/foreground rather than over/under?
<emilio> astearns: that also has the same issue
<emilio> fantasai: we could do that
<emilio> una: I can see where it's coming from but I don't know...
<emilio> chris: not sure it has the same ambiguity
<dbaron> this is why I like color-contrast(white over [blue red green], wcag2(AA))
<emilio> lea: can be icons too right?
<emilio> chris: the algorithm is only about text
<emilio> +1 to dbaron
<smfr> i suggest color-contrast(blue, red, green over white using wcag2(AA))
<lea> +1 to fantasai
<emilio> fantasai: we generally put arbitrary long lists at the end
<emilio> una: I'd make contrast algorithm first
<emilio> ... so wcag2, white over blue, green, red
<emilio> TabAtkins: the algorithm could be typed first
<emilio> fantasai: everything before the first comma can be reordered
<emilio> q+
<una> so you could write: `color-contrast(wcag(AA) white over, red, blue, green)`
<emilio> chris: algo can specified against the target, what if you don't have a target contrast?
<una> s/wcag(AA)/wcag2(AA)
<lea> Could we maybe use a keyword before <contrast-algo>+ ? E.g. using <contrast-algo>+? color-contrast(using wcag(aa) ...)
<fantasai> color-contrast(over white wcag(), red, blue green)
<fantasai> color-contrast(over white wcag(), red, blue, green)
<fantasai> or
<fantasai> color-contrast(over white wcag(max), red, blue, green)
<TabAtkins> not `white over`, that would be invalid
<chris> s/wcag(/wcag2(/g
<TabAtkins> specifically because the order you put it in reverses the meaning
<emilio> una: over before the value is hard to read
<astearns> q+
<emilio> dbaron: I think the bracketed list
<TabAtkins> (`over white` and `white over` read as exactly opposite things)
<dbaron> (because they are opposite things!)
<emilio> dbaron: I think the bracketed list was nice but some people don't like that
<astearns> ack emilio
<emilio> fantasai: I think it should parallel gradients
<emilio> lea: also color-mix()
<fantasai> emilio: given confusion about ordering of stuff, can we choose some fixed order?
<fantasai> emilio: if you can reorder this stuff, we are getting confused
<fantasai> TabAtkins: preposition and color have a fixed order
<fantasai> TabAtkins: but algorithms can be moved around
<fantasai> emilio: why do we want to specify multiple algorithms?
<fantasai> T
<fantasai> TabAtkins: example was, if tomorrow someone comes up with the best contrast algo that works perfectly
<fantasai> TabAtkins: but you also need to satisfy WCAG2 for legal reasons
<fantasai> TabAtkins: you will need to specify two algorithms
<fantasai> chris: 2 algos or multiple?
<una> q+
<fantasai> TabAtkins: 1 or more
<fantasai> TabAtkins: you have to pass all the thresholds
<fantasai> TabAtkins: if they disagree on white or black, we prioritize the first specified algo
<fantasai> una: Wouldn't that give us the same problem with color list last?
<fantasai> fantasai: they're not comma-separated, and you will likely only have one or two
<fantasai> una: could space-separate color list
<bramus> q+
<fantasai> TabAtkins: that would prevent us from adding any parameters to the list, which I don't think we want to do
<una> q-
<astearns> I think I’d prefer `white background` and `white foreground` meaning the color is in that slot
<emilio> astearns: I'm not happy with the proposition, I'd rather have a name
<emilio> ... because it's not misinterpretable
<astearns> s/name/noun phrase/
<bramus> q-
<emilio> fantasai: do we want to go for the full word of just fg/bg?
<bramus> had same concern as astearns
<emilio> una: don't mind that
<lea> not abbreviations please!
<emilio> ... I think it's more readable because color is first
<emilio> bramus: background / foreground is clear
<emilio> clearer to me*
<bkardell_> color-contrast(black foreground wcag(), red, blue, yellow) ?
<emilio> una: if we're having foreground/background what about if you have a border or so?
<bkardell_> is that the suggestion?
<emilio> fantasai: I think you pick your contrast algorithm and pick something
<astearns> q?
<emilio> usually it's not equal amounts of colors
<astearns> ack astearns
<lea> q+
<emilio> so that's what you'd use as background
<fantasai> s/usually/fantasai: usually/
<astearns> ack lea
<fantasai> s/so that/fantasai: so that/
<emilio> lea: given most contrast algos don't distinguish bg/fg, could we have a default?
<emilio> ... so that we don't need to specify?
<emilio> chris: so the first one can be one of them?
<emilio> TabAtkins: what about the algo that does care?
<emilio> ... why not make it gramatically required? Some algos don't care numerically but
<chris> wht nt uz abrvnz
<emilio> lea: also can we not do bg/fg (abbreviations)?
<dbaron> dbaron: why not make it grammatically required (just for the algorithms that require it)?
<emilio> ... also can we have a keyword before the algorithm so it's easier to read?
<emilio> TabAtkins: we usually don't do that unless needed for disambiguation
<emilio> fantasai: as long as it doesn't prevent reordering
<emilio> TabAtkins: proposal is a keyword to separate the algos from the rest of the stuff, like put the keyword "using" in front or so
<emilio> fantasai: we don't need it so I don't think we should add it
<emilio> TabAtkins: agreed
<bramus> `color-contrast(<color> [background|foreground] using <contrast-algo>+, <color>#)` then?
<una> +1
<emilio> dbaron: what I was suggesting is to require foreground / background only when using an algorithm that needs it, otherwise optional
<emilio> lea: that's what I meant
<emilio> TabAtkins: so it'd be grammatically invalid to not provide it?
<emilio> dbaron: yes
<florian> q+
<lea> q+
<TabAtkins> color-contrast([ [ <color> [background|foreground] ] && <contrast-algo>+ ], <color>#)`
<emilio> una: related to the following question, auto would always require it
<astearns> ack florian
<emilio> florian: some algos require foreground/background, are there other things?
<emilio> chris: not so far
<emilio> ... there's a proposal to add a third color
<emilio> florian: e.g. to get a numerical value of wcag you don't need font-size, but AA/AAA does
<emilio> ... does that become an input?
<emilio> chris: that's a good question and I think background/foreground is reasonably special
<bkardell_> q+
<emilio> florian: I suspect font-size is quite special too?
<emilio> TabAtkins: you can specify the level as well
<emilio> florian: if you know the font-size
<astearns> ack lea
<emilio> fantasai: since the algorithm takes params we could communicate there
<emilio> lea: that'd make the grammar harder to define that conditional-requirement
<chris> s/third color/third color which is the surround
<emilio> TabAtkins: we'd define it in prose
<emilio> TabAtkins: a bit annoying
<emilio> lea: if we use a default we don't have that problem
<emilio> TabAtkins: yeah but that means that people will forget and get confusing answers
<emilio> lea: maybe mandatory everywhere?
<emilio> TabAtkins: yeah
<emilio> fantasai: I think that'd be fine
<astearns> ack dbaron
<Zakim> dbaron, you wanted to disagree about mandatory everywhere
<emilio> dbaron: I think right now there's only one algo that needs it and is still in flux
<lea> +1 to TabAtkins
<emilio> TabAtkins: I think intuitively, the fact that a lot of algos don't use it is clear that they are garbage, because changing fg and bg significantly changes the perception of contrast
<dbaron> ok, makes sense
<emilio> lea: can we resolve about making bg/fg mandatory?
<astearns> ack bkardell_
<chris> q?
<emilio> bkardell_: lots of the examples colors are just color names
<emilio> ... part of the reason we're doing it at runtime is that you might not have known inputs
<emilio> ... I think the complex case is when you don't have a single color background
<emilio> ... e.g. if you look at the screen what the color name should be?
<emilio> TabAtkins: nobody has put down an algorithm to do that easily
<emilio> bkardell_: I think I disagree
<una> real example, say a button: `color-contrast(lightblue background using wcag2(AA), royalblue, navy, darkblue)`
<astearns> ack fantasai
<Zakim> fantasai, you wanted to say I'm less happy about typing "background" every time
<emilio> TabAtkins: in any case it's a much more complex problem
<lea> una: `using` was rejected
<emilio> fantasai: I'm happy to make things mandatory, but I'm against adding more boilerplate
<emilio> TabAtkins: that's why I was going for fg/bg
<emilio> lea: over/under aren't that much timing
<emilio> typing*
<emilio> TabAtkins: let's clear
<emilio> florian: front/back?
<TabAtkins> s/let's/less/
<emilio> fantasai: that'd work for me
<emilio> una: issue isn't the naming, was the position
<emilio> astearns: proposed resolution is that the first part of the syntax is color with front/back, applying to that color
<una> `color-contrast(lightblue back wcag2(AA), royalblue, navy, darkblue)`
<dbaron> do we want to consider fore/back rather than front/back?
<emilio> fantasai: as long as they're reorderable?
<TabAtkins> color-contrast( [ <color> && [front | back]? && <algo>? ], <color># )
<smfr> q+
<astearns> ack smfr
<lea> TabAtkins: <algo>+, not >
<una> dbaron -- I think that `fore` might be more confusing
<emilio> smfr: I think having the fore/back not applying to the chosen colors
<TabAtkins> yes, right, typo on my part
<lea> s/not >/not ?/
<emilio> ... logically I'd think "the color I wanna choose is the front color"
<emilio> astearns: so you'd like the modifier in the color list?
<emilio> florian: we had the same problem with gradient direction, to top left, or from top left?
<emilio> smfr: I kinda prefer under/over but we rejected that so...
<dbaron> I told you smfr wanted prepositions :-)
<emilio> TabAtkins: curious about why you think over/under apply to a different thing than front/back
<lea> yeah, I also liked that over/under read more like natural language :/
<emilio> smfr: because it's like writing an english sentence, over <foo> I can remember
<emilio> una: that's why I preferred using too
<fantasai> 1. over/under
<TabAtkins> color-contrast{start:<color>, position:[front|back], choices: <color>+ }
<fantasai> 2. front/back
<dbaron> IMO, "white front" is natural language but "front white" is not.
<fantasai> 3. something else
<florian> 2
<emilio> lea: what were arguments against over under again?
<emilio> chris: confusing order
<emilio> una: you could reorder the first three values
<emilio> fantasai: we wouldn't allow to reorder that
<smfr> i like ‘over|under <color>’
<emilio> +1 smfr
<emilio> fantasai: in gradients we do the same
<lea> +1 smfr
<una> 2
<lea> 1
<bramus> 2
<TabAtkins> 2
<emilio> 1
<fantasai> 1
<astearns> 2
<chris> 2
<miriam> 1
<smfr> 1
<dbaron> abstain
<bkardell_> abstain
<emeyer> abstain
<dholbert> abstain
<emilio> una: core issue is do we want to make sure it's required?
<dbaron> (BTW, with my [] suggestion there's an only-over option where you could write "green over [red white blue]" or "[red white blue] over green".)
<emilio> lea: we concluded it should be required
<emilio> astearns: no consensus
<emilio> TabAtkins: proposal to keep keywords undefined
<emilio> ... poll authors then come back
<dbaron> (6 votes for #2, 5 votes for #1, 4 abstain... since some people voted with /me and weren't logged)
<emilio> RESOLVED: Keywords undefined
<emilio> RESOLVED: Whatever keywords for foreground/background whatever they are are required
<emilio> RESOLVED: No keyword ahead of algorithm list
<emilio> lol
<emilio> TabAtkins: next is comma separation between color list and the rest
<emilio> chris: if I have `over white, green, red, blue` it looks confusing
<emilio> chris: I don't think it makes sense
<bkardell_> fantasai: I had some lag, but yes - what smfr said :)
<astearns> q?
<emilio> chris: that'd look awful
<emilio> TabAtkins: it'd have the keyword before
<emilio> fantasai: agree it's a problem
<emilio> dbaron: same
<emilio> florian: not sure what's better
<TabAtkins> precedence for / is that it's weaker than commas when mixed
<TabAtkins> we have several properties using that
<emilio> lea: right, alternatives are not better
<emilio> astearns: proposal to punt
<emilio> ... (until keywords are decided)
<smfr> +1 to lea
<emilio> lea: right now color-contrast sounds like it returns a contrast, not a color
<dbaron> I like contrasting-color()
<fantasai> contrast-color()
<emilio> ... can we change the name? color-contrasting / contrasting-color?
<emilio> fantasai: I don't like adding ing
<emilio> lea: I think contrast-color works fine
<emilio> una: I like it
<miriam> +1
<emilio> astearns: objections?
<ydaniv> +1
<emilio> chris: I think we had them and then we harmonized with color-*
<emilio> ... but agree it's better the other way

@bramus
Copy link
Contributor

bramus commented Aug 2, 2022

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.

@hidde
Copy link
Member

hidde commented Aug 2, 2022

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

@NateBaldwinDesign
Copy link

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?

@bramus
Copy link
Contributor

bramus commented Aug 3, 2022

These algorithm-specific parameters could be extra arguments to the function, e.g wcag3(75, font-size, weight)

@NateBaldwinDesign
Copy link

NateBaldwinDesign commented Aug 3, 2022

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)?

@VicGUTT
Copy link

VicGUTT commented Aug 3, 2022

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)

@Myndex
Copy link
Member

Myndex commented Aug 9, 2022

Quick Syntax Thoughts

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.


le deeper dive


Spatial Freq., Polarity, Adaptation...

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?

Let's talk polarity

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

  • For low spatial frequencies¹, i.e. large patches of color, very large bold text, big solid icons or buttons, polarity of a simple pair of colors is significantly less important, with the larger proximal field as more important as a factor driving field adaptation and resulting contrast constancy.
  • With high spatial frequencies (i.e. body text) the proximal field and adaptation is still important, but the local adaptation of the stimuli and immediately adjacent background are weighted higher and contrast constancy effects do not come into play until much higher contrast levels.

Putting Things on top of other things

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.

Positively negative

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.

Context of Use

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

Algorithm ID

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

Defaults and Keywords

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

Two to come, pair too

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.

THAT SAID

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.

Mid Grey L* vs Adaptation proximal field chart from contrast matching experiments

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:

  1. And for clarity here: a big bold text or big solid buttons are a combination of low and high spatial frequencies: the large color patch area could be called "low" but the sharp edge is "high". Think in terms of signal theory and a square wave. For purposes of the contrast discussion though, let's just refer to this as low SF.

Thank you for reading,

Andy

@NateBaldwinDesign
Copy link

@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 😇

@Myndex
Copy link
Member

Myndex commented Aug 9, 2022

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!

@svgeesus
Copy link
Contributor

svgeesus commented Aug 9, 2022

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

@NateBaldwinDesign
Copy link

NateBaldwinDesign commented Aug 9, 2022

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.

@svgeesus
Copy link
Contributor

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:

  • order by Lightness
  • enable changes
  • set number of swatches to 500 or so
  • choose WCAG2.1
  • choose Lstar

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.

@svgeesus
Copy link
Contributor

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?

@Myndex
Copy link
Member

Myndex commented Aug 10, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. Closed Accepted by CSSWG Resolution css-color-6
Projects
Status: Tuesday
Development

No branches or pull requests