w3c / csswg-drafts

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

[css-values]: Express conditional values in a more terse way #5009

Open jonathantneal opened 4 years ago

jonathantneal commented 4 years ago

Which active (or yet reviewed) proposals might allow CSS authors to express @media conditional values in a more terse way?

The following hypothetical code was widely shared among Twitter users this last week:

A picture shared on Twitter of code, which is written out after this link

@media (max-width: [1200px, 768px, 425px]) {
  .text-box {
    font-size: [24px, 20px, 16px];
    padding: [50px, 30px, 10px];
    margin: [50px 25px, 25px 12.5px, 12.5px]
  }
}

This particular hypothetical solution is described by the author as a “media query [that] is an array of sizes and your property value arrays are linked to those sizes*.

Crissov commented 4 years ago

Looks like something for a preprocessor (SASS/SCSS, LESS).

CyberAP commented 4 years ago

This would basically reserve square brackets in properties to be used exclusively in conjunction with media queries. It certainly looks like a preprocessor target.

jonathantneal commented 4 years ago

That is well pointed out, @CyberAP. This particular implementation is not feasible in actual CSS. I should have done more to point this out.

I want to reiterate that I’m looking for thematically similar proposals that would allow CSS authors to express conditional values in a more terse way. The hypothetical solution above and the attention it received effectively demonstrate this desire by authors, I believe.

Please forgive me if I butcher this next part. An additional request of mine is that we consider not (even passively) encouraging preprocessors to accomplish tasks that create syntactical interop issues with actual CSS. I can understand why it has been suggested; and your freely-given feedback means a great deal to me. My experience has been that popular userland experiments can positively and negatively impact standardization. When we encourage solutions to work alongside the language as much as possible, we drive experimentation that can lead to and help inform future standardization. However, if the interop is missing, sometimes those userland experiments actually get in the way. I believe both results have been the case with Sass and CSS (Sass has since been wonderfully responsive to interop issues). Again, this is something I should have done more to point this out in my original issue, and I hope my request does not discourage anyone.

EDIT: I changed " If that education is not there" to "However, if the interop is missing", because I think it makes a lot more sense. See, knew I’d goof it. 😬

LeaVerou commented 4 years ago

Why not set a variable in a media query and express the other values as calculations over it? Presumably they're not completely random numbers, but there is some logic to them. Why not make it obvious?

meduzen commented 4 years ago

Why not set a variable in a media query and express the other values as calculations over it? Presumably they're not completely random numbers, but there is some logic to them. Why not make it obvious?

It’s a good option that will work for some layouts, not for others. There’s not always a possibility to extract a logic across media queries.

tabatkins commented 4 years ago

So I'll start by saying I get the urge here - the current MQ situation is definitely a bit annoying. Either you have to duplicate your whole stylesheet into each MQ, meaning that the same rule shows up in different versions spread across the entire sheet (making it hard to ensure you keep everything up-to-date while editting), or you keep rules together but have to duplicate the MQ over and over again.

This syntax, tho, and likely anything close to it, is unworkable. It depends on the MQ you're differentiating on to be only a single condition's value, and the properties you want to set differently based on MQ to all be exactly the same (or else duplicate the same "initial" value across several of the clauses). And that's not even getting into the actual syntax shenanigans; maybe we could work this with a ; separator, if it was always setting the whole value for the property.

That said, I think the core problem will get better if we keep pushing on MQ improvements, namely (1) letting you nest MQs into style rules, and (2) letting you set custom MQs or full custom at-rules.

The example above is a lot better if written as:

.text-box {
  @media (--size:small) {
    font-size: 24px;
    padding: 50px;
    margin: 50px 25px;
  } @media (--size: medium) {
    font-size: 20px;
    padding: 30px;
    margin: 25px 12.5px;
  } @media (--size: large) {
    font-size: 16px;
    padding: 10px;
    margin: 12.5px;
  }
}

Or even, with a custom at-rule that just transforms itself directly into an @media rule:

.text-box {
  @--small {
    font-size: 24px;
    padding: 50px;
    margin: 50px 25px;
  } @--medium {
    font-size: 20px;
    padding: 30px;
    margin: 25px 12.5px;
  } @--large {
    font-size: 16px;
    padding: 10px;
    margin: 12.5px;
  }
}

Or going a step further, with a custom function that evaluates a predefined MQ and selects a value accordingly:

.text-box {
  font-size: --page-size(24px; 20px; 16px);
  padding: --page-size(50px; 30px; 20px);
  margin: --page-size(50px 25px; 25px 12.5px; 12.5px);
}

All of these should be possible in the future; the latter two would require a bit of JS to work, but not a complex amount.

Crissov commented 4 years ago

I was recently thinking whether CSS Values should include a value type for an indexed set, array, tuple, group, enum, whatever with multiple entries of the same basic type. They would need special functions, of course, to access the values or a reduction of them. I think MQ-dependent values like these could make a valid use case for them.

@media screen and (max-width: 1200px) {
  --index: 3; --screen: wide;
}
@media screen and (max-width: 768px) {
  --index: 2; --screen: normal;
}
@media screen and (max-width: 425px) {
  --index: 1; --screen: narrow;
}
.text-box {
    font-size: calc(4px * (3 + var(--index)) );
    padding: set(var(--index), 10px, 30px, 50px);
    margin: set(var(--screen), var(--margin));
  --margin: (narrow = 12.5px), (normal = 25px 12.5px), (wide = 50px 25px);
}
carlosame commented 4 years ago

What about defining a switch function:

switch(<integer [1,∞]>, <toggle-value>#)

If the first argument is not positive, the value becomes invalid (yes it could start from zero too). The other arguments (I chose the toggle-value from the toggle() function, not sure if there is something more general) are selected according to the first: 1 means the first toggle-value is returned, 2 the second, etc.

If the first argument evaluates to N and there are M toggle-values, with M<N, then the last toggle-value is used. The toggle-values could also contain var() functions, if the switch is resolved before the vars are substituted/evaluated.

carlosame commented 4 years ago

@Crissov I see that you edited your post to add a block which included this:

padding: set(var(--index), 10px, 30px, 50px);

If you have a specific proposal, I'd appreciate a description of it like I did with mine. Editing a previous post to add something that looks very similar to my proposal in the next message... does not help.

tabatkins commented 4 years ago

Hmm, a generic value switcher like that might make sense, yeah. Would let us hit the use-case pretty simply, without needing to wait for future Houdini stuff, and with a pretty minor syntax tax overall. Mechanics are no more complex than any other variable-related stuff.

Like I'd said earlier, pretty sure we'd need to use a ; as the value separator there, but otherwise yeah it's all good. Functionally identical to just setting several variables in the MQ, but letting you avoid naming the temporary values, and keep them clustered at point-of-use rather than spread across MQs.

Crissov commented 4 years ago

@carlosame I was just trying to explain “They would need special functions, of course, to access the values …” with pseudo-code. The “or a reduction of them” part is for instance found in #544, #905, #2826 and #4700.

bkardell commented 4 years ago

If we are going to talk about a switch like function solution I think this conceptually fits with the existing switch/context proposal in that it seems like the underlying machinery could let you write tab's custom thing with some context property (which of course needs a whole topic discussion, but so does tabs I think) something like

@media (max-width: [1200px, 768px, 425px]) {
  .text-box {
    font-size: switch(
        (breakpoint == 1) 24px; 
        (breakpoint == 2) 20px; 
        (breakpoint == 3) 16px;
    );
    padding: switch(
        (breakpoint == 1) 50px; 
        (breakpoint == 2) 30px; 
        (breakpoint == 3) 10px;
    );
    margin: switch(
        (breakpoint == 1) 50px 25px; 
        (breakpoint == 2) 25px 12.5px; 
        (breakpoint == 3) 12.5px;
    );
  }
}

So then this would let us unify underlying work and answer a bunch of questions 'together' rather than entirely disjointly. We could easily sugar this as something like..

@media (max-width: [1200px, 768px, 425px]) {
  .text-box {
    font-size: breakpoint(
        24px; 
        20px; 
        16px;
    );
    padding: breakpoint(
        50px; 
        30px; 
        10px;
    );
    margin: breakpoint(
        50px 25px; 
        25px 12.5px; 
        12.5px;
    );
  }
}

But at least a lot of the harder questions can be centralized?

carlosame commented 4 years ago

The main intent with my switch function is, precisely, to be able to somehow 'centralize' the switch at the place where the author is naturally putting the declarations, as @tabatkins mentions ("clustered at point-of-use"), reducing the verbosity of media rules.

So in a media rule you set an index-like property (there is also the possibility to mix it with some calc() tricks), and also add all the rules that are really specific for that media query.

Then, you may want to put the switch at the "main" sheet, and for example if you have three index levels you do not need to always specify three values:

margin: switch(var(--mq-level); 50px 25px; 25px 12.5px; 12.5px);
font-size: switch(var(--mq-level); 24px; 20px);

Which also means that if you add another level and forget to update some declaration(s), things keep working.

Your suggestion (which is somewhat similar to the original proposal in this issue), instead, implicitly builds something similar to a matrix of values, that in principle have to be always fulfilled.

Finally, my switch allows for invalidating the declaration, which may sometimes be useful.

jonathantneal commented 4 years ago

This whole switch thing sounds like a great resolution. I’m all for unifying underlying work, @bkardell, @carlosame, @tabatkins.

css-meeting-bot commented 4 years ago

The CSS Working Group just discussed 5009 Express conditional values in a more terse way.

The full IRC log of that discussion <dael> Topic: 5009 Express conditional values in a more terse way
<dael> github: https://github.com/w3c/csswg-drafts/issues/5009
<dael> TabAtkins: Right now if you want to change styles with MQ it's stable for a long time. As long as you're doing a complete rewrite or tweak small bits it's fine. Substantial touches like change colors for dark mode it's awk. Have to repeat all selectors and if you make edits to one spot have to do same across all media blocks.
<dael> TabAtkins: Jonathan asked for way to better keep conditional things close together and require less repetition when you need to modify
<fantasai> https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-620766100
<dael> TabAtkins: Suggestions in thread. One I liked is a bit down i nthe issue. It's a switch function that takes int as first argument and than ; sep arguments that are properties. Returns nth of those depending on first arg
<AmeliaBR> q+
<dael> TabAtkins: Intention is if setting up colors for high/low contrast you can set the mq to int use switch to call one of those and the specific properties. Let's you call them without repeating MQ or selectors. When you need an edit it's all there
<hober> q+ to ask how this relates to bkardell_'s switch() we talked about last week
<dael> TabAtkins: If adding more cases you go across switches and add stuff
<florian> q+
<dael> TabAtkins: I think I'm super happy with this. No reason to write it literally. If you write with a variable as first arg I like it a lot to push it to values space rather than keep at rules space
<emilio> q+
<hober> q-
<dael> hober: Relationship between this and bkardell_ switch() function?
<bkardell_> q+
<dael> TabAtkins: No direct relation. Functionality feels similar but selecting on an integer vs container size data are different. We'll use similar
<dael> TabAtkins: Similar values handling where they're var-like but there's no direct connection
<Rossen_> ack AmeliaBR
<dael> AmeliaBR: Disagree with that. I don't think it's a good idea. Original use case of making it easier to condense parallel declarations for MQ I thought TabAtkins original proposal looked nicest to be able to nest @media conditions in a declaration block.
<jensimmons> q+
<dael> AmeliaBR: Esp with being able to declare custom keywords that's compact and readable.
<dael> AmeliaBR: And integrer that's a toggle is not something I see as readable. COuld be used for the purpose if a generic switch function is available. might be okay bc I do think a generic switch is usefull in CSS. But if talking about generic switch it should be in a generic way.
<myles> q+
<dael> AmeliaBR: If we're talking about generic switch function it should be all use cases including container query usecase. Can we break the proposals into consituant parts we can combine up?
<dael> AmeliaBR: Proposal in container queries a key part is you can access dimension from layout. If a generic way to access that as a variable that's resolved at layout or used value time and a generic case switch function that's not resolved until layout we can put ti together and get bkardell_ proposal from last week.
<dael> AmeliaBR: But it could also be used with a regular variable.
<dael> AmeliaBR: I think by separating the 2 feature requests we can address both at same time
<TabAtkins> q+
<Rossen_> ack florian
<dael> florian: In the thread I think I see 2 variants of switch. One that's off the integer with ; separator which looks terse for short and confusing for long ones. Other is more explicit that looks more like a select case where you list
<dael> florian: If you're listing you an switch out a variable and also use keywords. If you make that explicit you don't have to limit to integers. COuld go the way where you can use tokens and move into other switch function.
<Rossen_> ack emilio
<dael> emilio: I had same comment as with switch. I agree it's a good idea but don't know why different from calc min and max
<dael> emilio: bkardell_ proposal becomes an optional value in how it's resolved later. THat way both proposals make sense
<dael> emilio: A weight to conditionals in calc has been proposed before. switch can be that.
<dael> emilio: I just don't think we should invent another var-like hting
<bkardell_> for reference https://gist.github.com/bkardell/e5d702b15c7bcf2de2d60b80b916e53c
<Rossen_> ack bkardell_
<dael> bkardell_: Pasted in a link to last week's proposal. Wanted to point out the opening and lots of words in there point that it's not simply about a point size. It's a single generic function that introduces the ability into the css lifecycle.
<dael> bkardell_: Maybe something to what emilio said but there's value in centralizing it. I don't know if he's done it yet but original poster said my comments would satisfy use cases.
<dael> bkardell_: I'll add that lajava has been doing concrete impl and we think from that standpoint it makes sense to do these things together
<Rossen_> ack jensimmons
<drousso> queue+
<dael> jensimmons: I think this is an interesting problem. I understand some frontend teams put your mobile styles in one file and desktop in another or in a whole other section. Than this problems is at its worst
<AmeliaBR> the nested media query example proposal: https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-620726494
<dael> jensimmons: Another way to do it is it's one styleshet and throw a MQ at one rule. That way you've got the original idea with a class of files. Nesting selectors is something sass has done for a long time. It's thrown people when they move back to css. I'd advocate for something more universal like nesting selectors and than you could write more efficient code. Any of these solutions require switching from different files.
<dael> jensimmons: Teams can do that today and switch their thinking. Question becomes if we had something like nested selectors and teams thought of it as one set of styles with conditinals what else would they need
<dael> jensimmons: Agree there's a danger this makes it too complicated where elegane of MQ is lost. Some teams are great but I don't know if we want central of how you do css to becomes something that is that complicated
<fantasai> s/are great/are really into math and variables and calculations, which is great/
<dael> jensimmons: Other idea I've seen some teams believe and the web is neutral on and should stay that way to do it correctly you should define all the breakpoints and every number should be those breakpoints. Variables make that more efficient.
<fantasai> s/central of/centrail idea of/
<fantasai> s/centrail/central/
<dael> jensimmons: I don't believe that's necessarilyt he right way and I don't htink we should add anything to css to make it harder to do any way.
<Rossen_> ack myles
<dael> myles: 10 days ago leaverou made a comment why we can't make variables stronger. I didn't see it addressed. I'd like to ask same question here.
<myles> https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-620424568
<dael> AmeliaBR: [reads]
<dael> AmeliaBR: I interpret it as putting onus on author to do calc rather than having it be a css syntax thing
<dael> florian: Doable today but not readable. If you have.a MQ at the top of your sheet and thank you have 50 numbers. THen you have border-top-width: size17.
<dael> AmeliaBR: And things you're switching might not be numbers. Might be grid areas or colors
<AmeliaBR> q?
<dael> TabAtkins: Accumulating responses
<bkardell_> jon's (the OPs) comments are on the issue now btw
<dael> TabAtkins: Not in order. myles and leaverou comment. THat's exactly what I proposed switch would do as long as i'm interp her correctly. She may instead think sit's math formula and I don't think that's always possible. If just saying use variables to do this that's what switch function does
<bkardell_> +1 to "nesting would also be good"
<dael> TabAtkins: Earlier, I thikn florian , about nested vs switch. I think being able to nest MQ insto style rules is necessary. If any changes you want to make based on a MQ are more than.a signle properties you want nested MQ. Gives you same benefit of cluster but lets you link together
<dael> TabAtkins: When doing single property value for something like colors it's still more overkill in syntax. I like switch to be as terse as possible
<dael> TabAtkins: Earlier comment about this akin to layout-based switch from bkardell_ . This really isn't. Anything based on layout has to be late in pipeline and limited in what it can adjust. You cannot adjust display based on layout. Fox is in the henhouse. But no reason you can't adjust display based on this. MQ can shift display but changing where it's assigned.
<dael> TabAtkins: Similar conceptually but different in practice. We shouldn't try and merge them into one thing
<Rossen_> q
<Rossen_> ack TabAtkins
<dael> TabAtkins: Moving on to this being requested by conditionals in calc, I kind of agree. Could do via calc conditionals. var = 1 var = 2 type. People can do move complex things. That works for me.
<florian> q+
<dael> TabAtkins: If this is a great additional reason to support conditionals in calc I can spec that out, it's fine.
<Rossen_> ack drousso
<faceless2_> q+
<dael> drousso: One thing I want to point out is this is not the only situation with a problem but anytime it has to do with newlines or whitespace it doesn't work well with things built into browser. Anything that relyings on newlines will not play nice in browser dev tools. Should be considered
<dael> TabAtkins: Syntaxtically no reason, but larger issue is all comma sep lists. I put bg on separate lines to make it more readable.
<dael> drousso: Agree but devs use the tools to write these thigns so some consideration for what the editing experience should be. Not saying they can't be fixed, but they've been this way for a long time and we shouldn't make it worse.
<Rossen_> ack florian
<Rossen_> Zakim, close queue
<Zakim> ok, Rossen_, the speaker queue is closed
<jensimmons> +1 to thinking through what the debugging experience is. Separate from the specific DevTools concern, I agree that making this natively quite-complicated needs to be only done while thinking through the debugging experience.
<emilio> TabAtkins: I have a patch for you for `switch(<index>, <v1>, <v2>, <v3>, ...)` :)
<Rossen_> +1 on tool working well with CSS!
<TabAtkins> Every language reinvents half of Lisp buggily, it's fine.
<jensimmons> Can we start with nesting MQs?
<Rossen_> ack fantasai
<dael> florian: Continuing on my earlier comment. Conditionals in calc has been asked. In calc you can express a number of calc, but we need to express things that resolve to a bool if we do this. feels slippery slope and we end up with a new language in calc since people want complexity. Lists with a clumsier syntax isn't something we should do. It's powerful but let's not jump in accidentally
<Rossen_> ack f
<dael> mike: Agree with florian. If you put in calc you restrict to calc. Conditionals in css is a great idea. Not sure calc is best place.
<jensimmons> and reach out to Sass to ask them why they haven't done this yet. See if they can — to get author experience
<dael> mike: This must have come up in a preprocessor surely, should we link to one of them?
<dael> TabAtkins: I know multiple have nested MQ, don't know if at value level
<TabAtkins> @jensimmons I'd ask you to do some examples on your own with light/dark/high-contrast/low-contrast MQs, and see if you'r ehappy with the amount of MQ repetition you need
<dael> bkardell_: Jon works on preprocessor stuff, the original poster. One of the points has to do with parsing. There's a difficult parser scenario where like URLs there's a spec but what people impl in not-browsers is not entirely accurate.
<AmeliaBR> Sass has `@if` and `@else` rules: https://sass-lang.com/documentation/at-rules/control/if
<dael> bkardell_: Worry about forward harm that can be done if we add new syntax.
<dael> Rossen_: I don't think we can get to a resolution here.
<lajava> TabAtkins, I don't think switch should be restricted to layout related conditions; I think from the implementation we can resolve the switch conditions during CSS parsing as well
<dael> Rossen_: TabAtkins since you brought this topic anything you want to see going forward?
<jensimmons> @ TabAtkins — I have written a lot of CSS.
<dael> TabAtkins: Right now happy to go ahead and table. I'll write up a more fleshed out proposal for my idea and we'll bring it up in a future call
<dael> Rossen_: Similar to 2 issues ago seems highly desired functionality and will take some ironing before we can resolve
fantasai commented 4 years ago

Link to @bkardell’s switch() proposal from a different issue: https://gist.github.com/bkardell/e5d702b15c7bcf2de2d60b80b916e53c

LeaVerou commented 4 years ago

If we do something like this, can it also cover use cases where you basically need a ternary instead of tying it to media queries? E.g. things like border-width: if(100% < 50vw, 1px, 2px). You can hack it with sign(), min() and max(), but it's awkward.

emilio commented 4 years ago

Yeah, my argument was on the line of @leaverou's comment.

FWIW, an index-based switch(index; v1, v2, v3...) function is just a couple lines of code, assuming that's also separately: https://hg.mozilla.org/try/rev/3744db19fd416ec99fbf96962811527f21e069e5 :)

tabatkins commented 4 years ago

One of the major objections during the call was the idea that we should just rely on nested MQs going forward, as that's already something we plan on doing anyway.

I don't think this is a good idea. We do need to be able to nest MQs inside of style blocks (for the same reason presented here, in fact!), for when adjusting to a MQ involves touching multiple properties at once. Trying to coordinate that by using a switch() in each property independently would be both hard to write and hard to read, bad idea in general.

But when one is just adjusting a single property according to an MQ (relevant to us here: adjusting colors for the combo of {light, dark}x{high-contrast, low-contrast}), using MQs gets real verbose real fast, even if we pretend that custom MQs or custom at-rules are already a thing.

Here's a real-world example, taken from the W3C stylesheet. It's a bit long, so I can show off the effect of multiple color properties being affected:

    .note {
        border-color: #52E052;
        background: #E9FBE9;
        overflow: auto;
    }

    .note::before, .note > .marker,
    details.note > summary::before,
    details.note > summary > .marker {
        text-transform: uppercase;
        display: block;
        color: hsl(120, 70%, 30%);
    }
    /* Add .note::before { content: "Note"; } for autogen label,
       or use class="marker" to mark up the label in source. */

    details.note > summary {
        display: block;
        color: hsl(120, 70%, 30%);
    }
    details.note[open] > summary {
        border-bottom: 1px silver solid;
    }

Here's the same code, using MQs and switch() to adjust colors for the aforementioned four combinations. (To avoid me having to actually do design work for the purpose of an example, I'm just using four arbitrary colors in every case.)

@media not (prefers-color-scheme: dark) and not (prefers-contrast: low) {
  :root { --c: 1; } // light, high-contrast
}
@media not (prefers-color-scheme: dark) and (prefers-contrast: low) {
  :root { --c: 2; } // light, low-contrast
}
@media (prefers-color-scheme: dark) and  not (prefers-contrast: low) {
  :root { --c: 3; } // dark, high-contrast
}
@media (prefers-color-scheme: dark) and (prefers-contrast: low) {
  :root { --c: 4; } // dark, low-contrast
}

    .note {
        border-color: switch(var(--c); #52E052; #52E052; #52E052; #52E052);
        background: switch(var(--c); #52E052; #52E052; #52E052; #52E052);
        overflow: auto;
    }

    .note::before, .note > .marker,
    details.note > summary::before,
    details.note > summary > .marker {
        text-transform: uppercase;
        display: block;
        color: switch(var(--c); #52E052; #52E052; #52E052; #52E052);
    }
    /* Add .note::before { content: "Note"; } for autogen label,
       or use class="marker" to mark up the label in source. */

    details.note > summary {
        display: block;
        color: switch(var(--c); hsl(120, 70%, 30%); hsl(120, 70%, 30%); hsl(120, 70%, 30%); hsl(120, 70%, 30%));
    }
    details.note[open] > summary {
        border-bottom: 1px switch(var(--c); hsl(120, 70%, 30%); hsl(120, 70%, 30%); hsl(120, 70%, 30%); hsl(120, 70%, 30%)) solid;
    }

And now here's the same code using nested MQs. To help it out, I'm going to go ahead and assume that MQ aliases exist, because otherwise it's just ridiculously verbose. (And similarly, I'm just spamming colors rather than do design work to actually do representative colors. ^_^)

@custom-media --light-high not (prefers-color-scheme: dark) and not (prefers-contrast: low);
@custom-media --light-low not (prefers-color-scheme: dark) and (prefers-contrast: low);
@custom-media --dark-high (prefers-color-scheme: dark) and  not (prefers-contrast: low);
@custom-media --dark-low (prefers-color-scheme: dark) and (prefers-contrast: low);

    .note {
        @media (--light-high) {
          border-color: #52E052;
          background: #E9FBE9;
        } @media (--light-low) {
          border-color: #52E052;
          background: #E9FBE9;
        } @media (--dark-high) {
          border-color: #52E052;
          background: #E9FBE9;
        } @media (--dark-low) {
          border-color: #52E052;
          background: #E9FBE9;
        }
        overflow: auto;
    }

    .note::before, .note > .marker,
    details.note > summary::before,
    details.note > summary > .marker {
        text-transform: uppercase;
        display: block;
        @media (--light-high) {
          color: hsl(120, 70%, 30%);
        } @media (--light-low) {
          color: hsl(120, 70%, 30%);
        } @media (--dark-high) {
          color: hsl(120, 70%, 30%);
        } @media (--dark-low) {
          color: hsl(120, 70%, 30%);
        }
    }
    /* Add .note::before { content: "Note"; } for autogen label,
       or use class="marker" to mark up the label in source. */

    details.note > summary {
        display: block;
        @media (--light-high) {
          color: hsl(120, 70%, 30%);
        } @media (--light-low) {
          color: hsl(120, 70%, 30%);
        } @media (--dark-high) {
          color: hsl(120, 70%, 30%);
        } @media (--dark-low) {
          color: hsl(120, 70%, 30%);
        }
        color: hsl(120, 70%, 30%);
    }
    details.note[open] > summary {
        border-bottom: 1px silver solid;
        @media (--light-high) {
          border-color: #AE1E1E;
        } @media (--light-low) {
          border-color: #AE1E1E;
        } @media (--dark-high) {
          border-color: #AE1E1E;
        } @media (--dark-low) {
          border-color: #AE1E1E;
        }
    }

I think the results are pretty clear here - even with the significant verbosity savings from a @custom-media, you're still turning every color-using property into roughly 8x as many lines, with tons of repetition across those (both repetition of the @media lines, and the property names; I also slightly rejiggered the border declaration in the final rule to avoid repeating the non-color values). The whole section goes from "easily fits on my screen" to "more than a screenful of content", with what is imo a lot of visual noise.

Contrast with the switch() example, where the color properties get longer (by a little more than 4x), but you don't gain any lines. (Longer starting declarations would probably encourage the author to linebreak between values, so you would increase the number of lines, but only by 4x, or maybe 5x if you keep the switch(var(--c), on the first line and put the values on subsequent ones.) There's also a bare minimum of repetition; the only repeated bit is switch(var(--c), in each value, because the logic behind the switching was centralized in the MQs at the start of the document and doesn't need to be repeated at each usage site.

And this was in just one small chunk of the W3C stylesheet - there are a lot more colors across the whole sheet than just these four instances. I think this example is both very realistic and fairly minimal; in real-world stylesheets I think there are often even more conditions than this.

tabatkins commented 4 years ago

If we do something like this, can it also cover use cases where you basically need a ternary instead of tying it to media queries? E.g. things like border-width: if(100% < 50vw, 1px, 2px).

Yes, we could. We could widen the grammar of the first argument to <integer> | <conditional> (defining a calc-ish conditional with the same grammar structure as MQ/supports), and then if you use a conditional we select either the first or second value if it's true/false. (We'd probably allow more than two values, but third onward just would never be selected by a conditional argument.)

emilio commented 4 years ago

To be clear, as @bkardell asked me to clarify: The implementation in my comment above is basically the switch() version (only I misunderstood @tabatkins, and I only used the semicolon for the index, so it's switch(<i>; v1, v2, v3, ...)). It does handle CSS variables as in Tab's examples just fine.

Of course the fallback I chose is quite debatable (just return the last argument if the index is negative or out of bounds), and so on, but... :)

tabatkins commented 4 years ago

Yeah, I think an invalid index would instead be a parse failure (subject to being able to determine that - using var() would make it iacvt instead, using calc() would clamp it into the 1-N range implied by the rest of the values, etc).

Loirooriol commented 4 years ago

I think an invalid index would instead be a parse failure

For consistency with calc() & friends I think I would expect this to be addressed like in https://drafts.csswg.org/css-values/#calc-type-checking

the value resulting from an expression must be clamped to the range allowed in the target context. Additionally, if a math function that resolves to <number> is used somewhere that only accepts <integer>, the computed value and used value are rounded to the nearest integer

tabatkins commented 4 years ago

If we think of this like a math function, yeah. If we think of it like a non-math function, then it would work as I said.

Either way works for me.

Crissov commented 4 years ago

Authors may want a switch like this to apply by

Iʼm pretty sure there are reasonable use cases for all of these, even if we do not have generic key-value arrays in CSS (yet). The question is: should they all be supported within a single function?

(I hate that most programming languages only have single-value case conditions in their switch statement, but I actually like its cascading nature, although most people treat a break statement as mandatory within cases.)

carlosame commented 4 years ago

Either way works for me.

I wonder @tabatkins why not keep what was specified in my proposal: clamp positive values (natural numbers) and invalidate for negative/zero.

It preserves the two desirable behaviours of explicit invalidation and clamping. Otherwise, you either have one or the other.

carlosame commented 4 years ago

I only used the semicolon for the index

Using the semicolons allow commas to pass through the switch, which is better than the "toggle-values could also contain var() functions, if the switch is resolved before the vars are substituted/evaluated" hack that I suggested in my proposal.

Then, the post-index arguments can be <declaration-value> instead of <toggle-value>.

I'm unaware if other functions using the semicolon as a separator have been suggested.

tabatkins commented 4 years ago

Authors may want a switch like this to apply by [...]

All but the first of those would require a key/value association in the function, rather than just a list (implicitly keyed by index), which would make the grammar more complicated.

On the other hand, k/v is required for the layout-time switching from bkardell's proposal https://gist.github.com/bkardell/e5d702b15c7bcf2de2d60b80b916e53c, so maybe something can be worked out.

I wouldn't want to complicate the common case by requiring each branch to be explicitly tagged with an integer, tho.

I wonder @tabatkins why not keep what was specified in my proposal: clamp positive values (natural numbers) and invalidate for negative/zero.

It preserves the two desirable behaviours of explicit invalidation and clamping. Otherwise, you either have one or the other.

Precisely because it's one-or-the-other, and thus consistent. Can you explain why you believe clamping overly-large values is useful?

I'm unaware if other functions using the semicolon as a separator have been suggested.

Nope, this is new.

carlosame commented 4 years ago

Can you explain why you believe clamping overly-large values is useful?

You won't typically clamp overly-large values, but values that exceed by one or two. I explained that in a previous comment, that you may not want to define a full matrix of values for every mq "level", and perhaps repeating the last value is good enough for quite a few definitions. And, copied from that post:

Which also means that if you add another level and forget to update some declaration(s), things keep working.

tabatkins commented 4 years ago

I explained that in a previous comment, that you may not want to define a full matrix of values for every mq "level", and perhaps repeating the last value is good enough for quite a few definitions.

Assuming that there's only one dimension you might want to omit (so you can put it at the end) seems a bit fraught, but...

Which also means that if you add another level and forget to update some declaration(s), things keep working.

...this is more interesting. I'm not entirely sure it's right tho - sometimes we do want things to fail obviously, to draw the author's attention to it and make it more likely to be fixed. Clamping makes the assumption that, in the case of this sort of mistake, the author would be okay with using whatever was last in their list, which has no guarantee of being appropriate to combine with the other values from the new set.

(Note that if we do go with "invalid", switch(calc(100000); ...) will indeed clamp to the last valid index, so at least authors could activate the behavior manually if they want.)

carlosame commented 4 years ago

sometimes we do want things to fail obviously

The declaration being invalid is not necessarily more obvious than the last value being applied. Depends on the document and the style sheet. Also, we are talking about media queries that could apply to media that is not being frequently tested/verified. The last value is the closest that we have to "author's intent".

switch(calc(100000); ...) [...] authors could activate the behavior manually if they want

If you mean putting calc() at the switch itself, that prevents explicit invalidation because calc() is supposed to clamp.

tabatkins commented 4 years ago

The last value is the closest that we have to "author's intent".

My point is that the last value isn't guaranteed to be closer to their intent than any other value.

That said,

Also, we are talking about media queries that could apply to media that is not being frequently tested/verified.

...this is pretty reasonable, and does lean me towards clamping rather than invalidating large values.

If you mean putting calc() at the switch itself, that prevents explicit invalidation because calc() is supposed to clamp.

Yes, that's what I was saying. ^_^

carlosame commented 4 years ago

I’m all for unifying underlying work

I believe that a proposal like yours @jonathantneal or @bkardell's is not incompatible with the integer-based switch.

As far as I'm concerned I'd like to see proposals like those being further discussed (I even have my own flavour of it), although I see that kind of functionality as something that fits important use cases, but not a full/only solution to the issue.

carlosame commented 4 years ago

does lean me towards clamping rather than invalidating large values

And also clamping negative/zero, or you handle them as invalid?

tabatkins commented 4 years ago

Zero/negative would always be an obvious authoring error, so it's appropriate to invalidate at that point. (Tho Oriol argues in https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-625565859 that it would be best to consider this as part of the family of math functions, and thus automatically clamp at both ends. I don't think I agree.)

carlosame commented 4 years ago

Zero/negative would always be an obvious authoring error

Not necessarily. Explicit invalidation is a potentially useful feature, forcing inheritance (on inherited properties):

@media (...) {
  [foo=bar] {
    --mq: 0;
  }
}

font-size: switch(var(--mq);...);

tabatkins commented 4 years ago

Well, you're arguing for invalidation as well, so that's fine. ^_^

But also, in this case you could also use any non-number value to make it invalid; you don't need "0" specifically to have that behavior.

carlosame commented 4 years ago

you don't need "0" specifically

True, although invalidating with "0" (or negative) allows conditional calc() tricks when defining the custom property.

bkardell commented 4 years ago

So... having had this conversation, including CSSWG and several follow on conversations I would like to revise my take from yesterday since my examples included a vector in the MQ to provide the context and I understand this better from several angles now...

Thinking about the two things being interchangeably referred to as switch() together is definitely useful. Seeing this case and the comments means that @javifernandez and I will continue to think about whether something like this could work - I can't see any reason it couldn't

@media not (prefers-color-scheme: dark) and not (prefers-contrast: low) {
  :root { --c: 1; } // light, high-contrast
}
@media not (prefers-color-scheme: dark) and (prefers-contrast: low) {
  :root { --c: 2; } // light, low-contrast
}
details.note > summary {
    display: block;
    color: switch(
                      (--c == 1) hsl(120, 70%, 30%); 
                      (--c == 2) hsl(120, 70%, 30%); 
                     default hsl(120, 70%, 30%); 
    }
}

I believe that is useful and super-powerful, but I also agree that if you have exactly this case (not uncommon) and not some more complex expression, that is unnecessarily verbose and inherently more complex. So, I think there's a lot of value (separately) to the 'index based' function that I agree with @tabatkins is actually different from an authors perspective and maybe from an implementation one too. Unfortunately with kind of a lot of thought I don't see any way they can both be called switch() and if they were it would be unnecessarily complex - I guess that's why we have things like if and switch in languages which do kind of similar things in different ways... I think here too both are valuable.

I think you could make a compelling argument for either of them to be called 'switch' tbh, but I not-entirely-mildly prefer to keep my thing called switch as I lean toward 'it is slightly more switch-like in ways that matter to me' and, to a lesser extent because it's been circulating and I feel like delivering something else called that now will add confusion.

@emilio seemed to imply he wasn't attached to the name, a quick straw poll of people thought that this seemed rather like if(), but idk... can we at least agree that they do different things and continuing to call both switch() seems bad? case()? index-switch()? something else?

Loirooriol commented 4 years ago

For me the idea in a switch statement is that you have some conditions defining different cases, and you choose the 1st case whose condition holds.

That applies to the container queries switch(), but not to the switch() in this issue. This one is just picking the nth value among a list of values. Calling this switch() would be confusing IMO, I agree it needs another name. Maybe something like pick-nth(), nth-value(), get-at() or similar.

carlosame commented 4 years ago

I don't see any way they can both be called switch() [...] to a lesser extent because it's been circulating

Your gist was created 5 days before I posted my switch (I was unaware of your other switch), and this is not a lot of time to circulate. I do not think that any of the two owns the "switch" word, and my opinion is that if any of the proposals is adopted by the CSSWG (and that remains to be seen), the name should be decided based on the convenience of CSS authors, and not "trademark wars".

That said, the proposal that has quite a few conditionals on it (and to me, looks like a multiple if()), is yours :slightly_smiling_face:.

IMHO we should focus on creating proposals that are useful to authors, and not on telling other people to change the names of their stuff.

For me the idea in a switch statement is that you have some conditions defining different cases, and you choose the 1st case whose condition holds.

A switch with multiple conditions is not what people using C, Java, etc. are used to. Could you point to an example of a widely-used "switch" that has multiple conditionals?

Loirooriol commented 4 years ago

Could you point to an example of a widely-used "switch" that has multiple conditionals?

Well one could argue that the conditions are implicitly defined to be equality checks between a fixed value specified at the top and another value for each case.

So maybe you don't specify the full conditions explicitly, but at least they are there in the idea. Picking the nth value in a list can be converted into a switch form but it seems a different idea to me.

https://en.wikipedia.org/wiki/Switch_statement#History says that the origin of the switch is the "definition by cases":

"#F. The function φ defined thus

φ(x1 , ... , xn ) =
    φ1(x1 , ... , xn ) if Q1(x1 , ... , xn ),
    . . . . . . . . . . . .
    φm(x1 , ... , xn ) if Qm(x1 , ... , xn ),
    φm+1(x1 , ... , xn ) otherwise,

where Q1 , ... , Qm are mutually exclusive predicates (or φ(x1 , ... , xn) shall have the value given by the first clause which applies) is primitive recursive in φ1, ..., φm+1, Q1, ..., Qm+1.

So there you are, Q1 , ... , Qm are the conditions. For a specific programming language example, the following is a common pattern in JS:

switch (true) {
  case cond1(): /* ... */ break;
  case cond2(): /* ... */ break;
  case cond3(): /* ... */ break;
}
carlosame commented 4 years ago

origin of the switch is the "definition by cases"

Yes, cases on a single value/expression. But have you read the other proposal? Cases are independent conditions. Let's put it this way: which of the following two blocks of code looks more like a C switch:

int mq;
switch (mq) {
case 1:
  return value1;
case 2:
  return value2;
case 3:
default:
  return value3;
}

which is conceptually similar to my switch, or this:

grid-template-columns: switch(
        (available-inline-size > 1024px) 1fr 4fr 1fr;
        (--foo == 1) 2fr 1fr;
        (--bar < 3) 1fr;
        default 1fr;
);

This last block is valid according to the other "switch" syntax. I mean, there are multiple <switch-condition> <css-value> pairs, and nowhere says that <switch-condition> has to operate on the same variable nor with the same comparison operator.

Loirooriol commented 4 years ago

Well, your 1st block of code is an actual C switch and the 2nd one is CSS, so it's unfair. But your CSS switch doesn't look like that at all. It be converted into that form, but IMO conceptually it's closer to vector::at.

And while in C switch doesn't allow the conditions of your 2nd block, in JS you can. And the concept is basically the same.

MatsPalmgren commented 4 years ago

Minor nit: default is equivalent to (0 < 1) so it doesn't seem worth it to add special keyword for that. default just adds cognitive burden IMO (what happens if it's not the last conditional?). It's likely easier to implement without that special case too.

carlosame commented 4 years ago

it's unfair. [...] in JS you can

Well, what is probably unfair is to put a JS example where an if()-else block is shoehorned to a switch.

In any case, I'm not the one taking the final decision on what will be adopted, but saying that my function is an if() and not a switch() (while the other proposal would be a 'real' switch) is biased at best.

tabatkins commented 4 years ago

So there's three distinct functions being bandied about here.

  1. The one mentioned by Carlos, where you provide a list of <any-value>s and an index, and it resolves to the nth value; this function can be used anywhere. It's also almost certainly var()-like (in that it makes the property always-valid at parse time as long as the overall function is valid, turns on iacvt behavior, etc.)†

    color: nth-value(var(--x); red; blue; yellow);

    (There's a minor variant where you supply a value, and then pairs of values to match against and <any-value>s, selecting the first pair whose match-value is identical to the first arg. This allows for named keys, which can be more readable in some cases than just a list, but is also more verbose for technically no additional functionality. I'll consider this off the list for now, but keep it on the back-burner just in case.)

    color: nth-value(var(--x); (light-low) red; (dark-low) blue; (light-high) yellow);
  2. The one mentioned by Lea, which is a math function you provide pairs of calc()-ish comparisons and calc()-ish results, and it resolves to the first value whose comparison succeeds; this can be used anywhere a calc() can. (Just like the other math functions, we can tell what its type will be at parse-time by examining the calculations and grammar-check that accordingly, so it doesn't need to be var-like.)

    margin-left: cond((50vw < 400px) 2em, (50vw < 800px) 1em, 0px);
  3. The one mentioned by Brian, where you provide pairs of comparisons (more powerful than what calc() can access; basically what a Layout Worklet is provided) and <any-value>s, and it resolves to the first value whose comparison succeeds; this function can be used in a restricted set of safe properties that are "downstream" of the values it's allowed to compare over. (Most CSS properties are "safe", tho.) It's also almost certainly var()-like.†

    grid-template-columns: switch(
        (available-inline-size > 1024px) 1fr 4fr 1fr;
        (available-inline-size > 400px) 2fr 1fr;
        (available-inline-size > 100px) 1fr;
        default 1fr;
     );

All three seem to do something useful and distinct; none of them can reasonably be folded into the other without losing significant amounts of functionality.

As I did in the examples above, I suggest for the purposes of discussion we call (1) nth-value(), (2) cond(), and (3) switch().


† If we restrict the functions to only be usable as the whole value of a property, they don't need to be var-like; the UA can instead just grammar-check each of the <any-value>s against the property's grammar at parse-time. This may be a reasonable imposition. That said, nth-value() will really only be used with a var() or similar as its first argument anyway, so the question is moot in practice for it; it's still potentially relevant for switch(), tho.

tabatkins commented 4 years ago

Also, fwiw, I did some exploration on handling switch() with an at-rule instead of a function, because I think the function flirts with too much visual complexity: https://gist.github.com/tabatkins/7d0d55fe08812d28cc46bed40b9deacb

frivoal commented 4 years ago

2 and 3 seem pretty similar to me. Just to clarify, is it right that the only differences are:

If so, 3 just seems like a more general version of 2, rather than "none of them can reasonably be folded into the other". Or am I missing something?

LeaVerou commented 4 years ago

Also, fwiw, I did some exploration on handling switch() with an at-rule instead of a function

I can see the utility, but it would be good to also have a quick function, for cases where it really is a one-off. The @rule can be too much repetition for a number of common cases.