w3c / csswg-drafts

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

[css-color] Add a function to allow authors to specify colors reacting to `color-scheme` #7561

Open emilio opened 1 year ago

emilio commented 1 year ago

Some UIs require rendering a light button in some places, but dark in another. It'd be nice if you could switch between these by using color-scheme, like you can with native widgets. However, right now there's no way to do this other than exclusively using system colors.

Chromium has -internal-light-dark(<light-color>, <dark-color>) for this. However color-scheme was designed a bit more generically than light / dark, so maybe we need a schemed-color(<custom-ident> <color>, <fallback-color>) or so, where the <custom-ident> is the scheme name, and the <color> is the color for that scheme. So -internal-light-dark(red, blue) would become schemed-color(light red, blue) or so.

This would allow authors to create widgets that properly react to color-scheme.

bramus commented 1 year ago

Big fan of the -internal-light-dark() function here as well. Would make some things easier.

A few questions/remarks:

  1. With the proposed schemed-color(<custom-ident> <color>, <fallback-color>), how would support for multiple schemes work?

    Do you suggest to nest schemed-color() – e.g. schemed-color(light red, schemed-color(dark blue, pink)) – or should we allow multiple entries by changing the syntax to schemed-color([<custom-ident> <color>]#, <fallback-color>)?

  2. The suggested schemed-color only caters for colors, whereas light-dark() allows things other than colors. This seems pretty limiting. Can we open it up to have it return any value instead of a <color>? So the function would maybe become schemed-value()?

    I’m not an expert on CSS syntax, but I guess the syntax would then become this: schemed-value([<custom-ident> <declaration-value>]#, <declaration-value>)

    Anecdotally: The function in Chromium first was called -internal-light-dark-color() but then got renamed to -internal-light-dark()

  3. Maybe light-dark() could be kept, and become a alias (limited to only two schemes) for schemed-color?

    Or if we should not want to pursue this, maybe we can leave it up to authors to define their own custom function once that becomes a thing?

  4. With CSS Nesting – and potentially implicit & { } along with that – around the corner, this would become more easy for authors to do:

    1. No Nesting:

      el {
        --img: url(/img/bg-light.png);
        background: transparent var(--img) no-repeat 0 0;
      }
      
      @media (prefers-color-scheme: dark) {
        el {
          --img: url(/img/bg-dark.png);
        }
      }
      
      @media (prefers-color-scheme: other) {
        el {
          --img: url(/img/bg-other.png);
        }
      }
    2. With nesting:

      el {
        --img: url(/img/bg-light.png);
        background: transparent var(--img) no-repeat 0 0;
      
        @media (prefers-color-scheme: dark) {
          --img: url(/img/bg-dark.png);
        }
      
        @media (prefers-color-scheme: other) {
          --img: url(/img/bg-other.png);
        }
      }

    However, the nesting version doesn’t beat schemed-value() imo, so authors would still benefit from having such a function available:

      el {
        --img: schemed-value(
          dark url(/img/bg-dark.png),
          other url(/img/bg-other.png),
          url(/img/bg-light.png)
        );
        background: transparent var(--img) no-repeat 0 0;
      }
emilio commented 1 year ago

I propose to go for light-dark(<foo>, <bar>), then extend to schemed-color or schemed-value in the future, if needed?

I'd propose to do this for <color> (or <color> and <image>) for now, rather than everything.

tabatkins commented 1 year ago

lol, schemed-color() rules as a name. (But probably isn't what we want to go with. ^_^)

But given that we purposely designed color-scheme to be extensible for the future, we should do the same here. (But I think we can optimize too.)

I propose the grammar be <color>#{2} | [ <custom-ident> <color> ]#{2,} - the first branch is just light and dark, the second lets you name the color schemes you want to associate it with.

mirisuzanne commented 1 year ago

In many cases, authors would want to toggle a single channel value (e.g. lightness) within a color. They can do that by repeating the entire color - but allowing <number>/<length> sure would simplify that use-case.

tabatkins commented 1 year ago

We need to either know the value type ahead of time, or lean on var() semantics. So we could do the schemed-color() / schemed-value() distinction that Bramus suggests, with the two behaviors.

emilio commented 12 months ago

In many cases, authors would want to toggle a single channel value (e.g. lightness) within a color. They can do that by repeating the entire color - but allowing <number>/<length> sure would simplify that use-case.

Yeah I didn't want to open this to arbitrary lengths and such because that adds a bunch of new property dependencies that I haven't thought through... Though using var() semantics probably works for that case?

lilles commented 11 months ago

However, the nesting version doesn’t beat schemed-value() imo, so authors would still benefit from having such a function available:

   el {
     --img: schemed-value(
       dark url(/img/bg-dark.png),
       other url(/img/bg-other.png),
       url(/img/bg-light.png)
     );
     background: transparent var(--img) no-repeat 0 0;
   }

Additionally, schemed-value() can do something the media query cannot, because prefers-color-scheme matches the same across the document while schemed-value() respond to the per-element color-scheme:

<style>
  div { background-color: schemed-value(dark red, light green); }
</style>
<div style="color-scheme:light">Green background</div>
<div style="color-scheme:dark">Red background</div>
css-meeting-bot commented 11 months ago

The CSS Working Group just discussed [css-color] Add a function to allow authors to specify colors reacting to `color-scheme`, and agreed to the following:

The full IRC log of that discussion <fantasai> emilio: discussed briefly in F2F
<fantasai> emilio: Tab's proposal about having a variable-like thing ...
<fantasai> emilio: I'm not sure this is ready for resolution. I added Agenda+ F2F and we didn't remove.
<fantasai> TabAtkins: I'm happy to discuss my proposal. Two things to consider
<fantasai> TabAtkins: The first and easiest one is, just having a color function that responds to color scheme
<fantasai> TabAtkins: this doesn't require anything fancy
<fantasai> TabAtkins: Proposed syntax is ...
<TabAtkins> color-scheme( <color>#{2} | [ <custom-ident> <color> ]#{2,} )
<fantasai> TabAtkins: You can either provide two colors
<fantasai> TabAtkins: or provide color scheme ident and color pairs
<fantasai> TabAtkins: This matches behavior of existing function that browsers have access to
<fantasai> TabAtkins: I know that Chrome and Firefox have something like a light-dark() function
<fantasai> TabAtkins: it's simply a color, goes where colors go, responds to color scheme
<fantasai> TabAtkins: other one is if you want to do other stuff, e.g. turn shadows on
<fantasai> TabAtkins: and that requires var-like semantics
<fantasai> TabAtkins: but simple color one will solve a majority of cases we care about here and be very simple
<fantasai> fantasai: If you only spec 2 colors, and we later introduce a new color scheme, what happens?
<emilio> fantasai: If you only specify two colors and then we add a third color-scheme, then what?
<fantasai> TabAtkins: suggest to default to "light"
<fantasai> TabAtkins: same issue applies with the list option
<fantasai> TabAtkins: in that case, maybe choose first one. or maybe choose "light" if specified
<Rossen_> q?
<Rossen_> ack fantasai
<emilio> fantasai: one of the nice things about ??? is that it makes very explicit what goes where
<emilio> ... like minmax() in grid
<emilio> ... so I understand that at some point for more schemes we want to tag individual values
<TabAtkins> s/???/chrome's internal function/
<emilio> ... but currently we just have light and dark
<emilio> ... I think it's a lot better to have light-dark like chrome has
<bramus> q+
<TabAtkins> q+
<emilio> ... at some point in the future that could become effectively syntactic sugar for a more-comprehensive function
<emilio> ... but even if we have that we could have a light-dark() since that's the more common thing
<Rossen_> ack bramus
<fantasai> bramus: I want to support what fantasai said, start with light-dark() and have it be syntactic sugar for color-scheme() or whatever in the future
<TabAtkins> I'm fine with `light-dark()` even if we have more schemes in the future, it's still a reasonable name.
<fantasai> bramus: Internally Chromium had light-dark-color(), but then changed to light-dark() which allowed using in properties other than colors
<Rossen_> ack TabAtkins
<fantasai> TabAtkins: I'm fine with light-dark() as a name. That's still fine if/when we do define more color schemes
<fantasai> TabAtkins: For having a second function that has var-like semantics, but can do anything ...
<Rossen_> q?
<fantasai> TabAtkins: makes sense to me, but that should be a separate thing so that we have a well-defined <color> that can be accepted in places that accetp colors
<fantasai> +!
<fantasai> +1
<fantasai> emilio: Shoudl we resolve on light-dark-color() and potentially extend to more color schemes or value types in the future?
<emilio> I'd go just for light-dark() as a name fwiw
<emilio> but light-dark-color() wfm
<fantasai> Rossen_: Proposed resolution is to add a light-dark() or light-dark-color() color ... extend for other color schemes later
<TabAtkins> light-dark-value() for the "anything" one?
<fantasai> dholbert: if we do want to add an additional function name, light-dark() sounds like a more generic thing
<fantasai> dholbert: so might want to use light-dark-color() for the color-specific thing
<fantasai> dholbert: but that's just naming thing
<Rossen_> ack fantasai
<emilio> fantasai: thought about what dholbert just said, but the color is going to be by far the most commonly used thing
<emilio> ... so might make sense to have light-dark() for color and something longer for the value syntax
<emilio> dholbert: makes sense
<emilio> fantasai: poll on the name?
<fantasai> Rossen_: Let's pick one and discuss in the issue.
<TabAtkins> (and note proposals to, say, name nth-value() to grab an arbitrary value from the list. "value" as an indicator of arbitrary, var-like behavior is somewhat leaned toward as a pattern.)
<TabAtkins> 1) light-dark-color()
<fantasai> POLL: 1) light-dark-color 2) light-dark
<TabAtkins> 2) light-dark()
<fantasai> 2
<emilio> 2
<TabAtkins> 2
<bramus> 1
<miriam> either
<dholbert> 2, per fantasai's note on making-the-common-thing-easy
<rachelandrew> 2
<oriol> abstain
<masonf> 2
<bramus> q+
<fantasai> bramus: What about schemed-color() etc.? Would we then have schemed-color() map to light-dark() and schemed-value() to light-dark-value() ?
<fantasai> TabAtkins: more color schemes in the future, but light-dark is just for these schemes
<Rossen_> ack bramus
<fantasai> bramus: returns a color?
<fantasai> TabAtkins: yes. If we want value, we can use light-dark-value()
<fantasai> RESOLVED: Add light-dark() fuction that returns a color based on the color scheme.
<fantasai> s/fuction/function/
jjcm commented 9 months ago

Just saw this from @bramus's post, and I suspect that things are already closed / there's no changing things now, but I see this as an approach that doesn't actually solve for the issues people are facing with theming, and does so in a way that will create a trap for them when pursuing proper theming support.

As a very simple example, what happens when a site that uses this grows to the point where they need to add a high contrast mode? At that point this will require a refactor.

The other element I'm concerned with here is it all but seals the deal that color-scheme will never grow beyond light|dark, despite custom identifiers being allowed for that field. What I'd much prefer is a solution that scales, rather than something that only helps one specific use case.

As a simple example, consider if it were implemented this way instead:

:root {
  color-scheme: light dark high-contrast;
}

body {
  background: scheme(light #eee, dark #333, high-contrast #fff);
}

With this model, you have something that scales beyond light and dark, but now supports visual accessibility modes such has high contrast/colorblind modes. You could also support multiple brands with this, i.e.

:root {
  color-scheme: docs spreadsheets slides;
}

body {
  color: scheme(docs blue, spreadsheets green, slides yellow);
}

We shouldn't ship single-purpose tools for the browser, but rather ones that scale and we can build upon.

bramus commented 9 months ago

things are already closed […]

I think this was mistakingly done so. The end goal is to have something like schemed-value(), with light-dark() being an intermediary step towards the final solution.

This is also reflected in the meeting minutes: go for light-dark() now, and extend to more color schemes and other types of values in the future.

(I’m hereby reopening the issue)

As a simple example, consider if it were implemented this way instead:

[…]

The syntax you propose rhymes with was suggested in the original post. I like the suggestion to have the function accept an arbitrary number of scheme-value pairs. Much handier than needing to nest one schemed-value() into the other.

Side note: I’ve added an extra section to my post to explicitly mention that light-dark() is not the end goal but schemed-color()/schemed-value() is. This will hopefully remove some of the confusion.

emilio commented 9 months ago

As a very simple example, what happens when a site that uses this grows to the point where they need to add a high contrast mode? At that point this will require a refactor.

I've always thought of things like high-contrast as somewhat orthogonal to light/dark, for what is worth. Something can still be light or dark and high-contrast.

For the record, I think light-dark is still pretty useful for high contrast. Think of the simple example:

:root {
  --text-color: light-dark(#ddd, #333);
  @media (prefers-contrast) {
    --text-color: light-dark(black, white);
  }
}

The other element I'm concerned with here is it all but seals the deal that color-scheme will never grow beyond light|dark, despite custom identifiers being allowed for that field. What I'd much prefer is a solution that scales, rather than something that only helps one specific use case.

FWIW, my understanding is that color-scheme was never intended to provide custom theme capabilities like suggested above, but is a way to expose the browser's color-scheme support. It might be worth filing an issue to discuss that, if that's something people want.

As a simple example, consider if it were implemented this way instead:

:root {
  color-scheme: light dark high-contrast;
}

body {
  background: scheme(light #eee, dark #333, high-contrast #fff);
}

As @bramus said, we did have this discussion. light-dark is still fairly useful regardless, and allows you to exploit the color-scheme property properly in the way the browser already does with system colors.

Anecdata, but in my experience with the Firefox front-end it's gotten quite a bit of adoption (see uses).

Firefox does support all sorts of themes, and that didn't make us not use this. Quite the opposite, it did simplify the theming setup quite a bit, from icons, to link colors, etc...

As soon as you have a complex enough theming setup, you need parts of the UI to be light and parts to be dark, and light-dark allows you to react to these like the browser does.

It seems you kinda want a more general theming mechanism than what the color-scheme property provides. And that's fair, maybe we should extend that property to not only expose built-in color schemes, or have a separate mechanism, or something. I encourage you to file an issue about it (with or without a proposal, that's ok). But FWIW I don't think that makes light-dark() here less useful (though I'm obviously somewhat biased).

I'm happy to hold off shipping light-dark() on Firefox if people thing it's fundamentally wrong, or a step in the wrong direction that would prevent us from doing something that we might want like extending color-scheme to support custom author-provided palettes, somehow, but I don't think it's the case.

svgeesus commented 9 months ago

As part of dealing with this issue:

I looked again at the definition of <color> and in particular <absolute-color-base> which is used, for example, in the definition of the override-colors descriptor in @font-palette-values.

<absolute-color-base> is a subset of <color> which excludes the following values, all of which have special handling (like depending on layout, resolving late, not having a defined colorimetric interpretation):

A consequence of adding light-dark() directly to <color> rather than to <absolute-color-base> is that it can't be used in, for example, override-colors. I suspect this was unintentional, and propose to add it to <absolute-color-base> instead (along with color-mix() which is also missing from the grammar.

@emilio @bramus was the intent to exclude light-dark() from use in override-colors? Or was that unintentional and I should move it?

bramus commented 9 months ago

I'm happy to hold off shipping light-dark() on Firefox if people thing it's fundamentally wrong, or a step in the wrong direction that would prevent us from doing something that we might want like extending color-scheme to support custom author-provided palettes, somehow, but I don't think it's the case.

The sentiment on social media (X, Mastodon) is generally in favor.

Recurring comments I’ve heard are:

These issues seem both unrelated to light-dark() itself.

@emilio @bramus was the intent to exclude light-dark() from use in override-colors? Or was that unintentional and I should move it?

I can’t gauge the consequences of doing that, so I’ll leave that decision up to Emilio and one of the color experts here – a certain Chris, I think you know him ;) – to make.

emilio commented 9 months ago

@svgeesus: light-dark() resolves late, based on the computed color-scheme. So unless @font-palette-values can provide a color-scheme, it can't be there, right? color-mix() can contain <currentColor> recursively, so I suspect it's a bit more complicated than just moving it there.

svgeesus commented 9 months ago

Aha okay, I see. It is in the right place, then.

But yeah I was just discussing with @LeaVerou color-mix() and RCS already break the assumption that the absolute ones are absolute. You can always color-mix(in oklab, peru, currentColor) or rgb(from currentColor r g b). So what <absolute-color-base> was trying to achieve by way of grammar needs to be expressed in prose, it seems. (Or a really, really unweildy and unreadable grammar),

LeaVerou commented 9 months ago

What I proposed to @svgeesus was invert this breakdown:

<color> = <color-function> | <hex-color> | <color-relative-basic> | <named-color> | ...
<color-relative-basic> = currentColor | <system-color> | device-cmyk()

Then also define a <color-relative> token, with prose along the lines of "Any <color> which includes <color-relative-basic> anywhere in its tree (note that this could be described as a grammar, it would just be a grammar that no human would want to ever read or edit). Then that token can be referenced as needed, from any other spec.

Another advantage of this is that it maintains a coherent definition of <color> abstracting away only the weirdness into a separate token, whereas the current breakdown has the weirdness up front and center.

emilio commented 9 months ago

Yeah it's probably worth defining the concept of "absolute-color" or something in prose.