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-values]: Express conditional values in a more terse way #5009

Open
jonathantneal opened this issue Apr 27, 2020 · 81 comments
Open

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

jonathantneal opened this issue Apr 27, 2020 · 81 comments

Comments

@jonathantneal
Copy link
Contributor

jonathantneal commented Apr 27, 2020

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
Copy link
Contributor

Crissov commented Apr 27, 2020

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

@CyberAP
Copy link

CyberAP commented Apr 27, 2020

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
Copy link
Contributor Author

jonathantneal commented Apr 27, 2020

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
Copy link
Member

LeaVerou commented Apr 28, 2020

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
Copy link

meduzen commented Apr 28, 2020

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
Copy link
Member

tabatkins commented Apr 28, 2020

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
Copy link
Contributor

Crissov commented Apr 28, 2020

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
Copy link

carlosame commented Apr 28, 2020

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
Copy link

carlosame commented Apr 29, 2020

@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
Copy link
Member

tabatkins commented Apr 29, 2020

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
Copy link
Contributor

Crissov commented Apr 30, 2020

@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
Copy link
Contributor

bkardell commented May 7, 2020

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
Copy link

carlosame commented May 7, 2020

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
Copy link
Contributor Author

jonathantneal commented May 7, 2020

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

@css-meeting-bot
Copy link
Member

css-meeting-bot commented May 7, 2020

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//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//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//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//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
Copy link
Collaborator

fantasai commented May 7, 2020

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

@LeaVerou
Copy link
Member

LeaVerou commented May 7, 2020

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
Copy link
Collaborator

emilio commented May 7, 2020

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

@astearns astearns removed the Agenda+ label May 7, 2020
@tabatkins
Copy link
Member

tabatkins commented May 8, 2020

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
Copy link
Member

tabatkins commented May 8, 2020

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
Copy link
Collaborator

emilio commented May 8, 2020

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
Copy link
Member

tabatkins commented May 8, 2020

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
Copy link
Contributor

Loirooriol commented May 8, 2020

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
Copy link
Member

tabatkins commented May 8, 2020

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
Copy link
Contributor

Crissov commented May 8, 2020

Authors may want a switch like this to apply by

  • numeric index (probably one-based): 1, 2, 3
  • numeric index range: 1-2, 3 and up
  • logical condition (effectively resulting in a numeric index range): <2, >3
  • arbitrary key: 1, foo, bar
  • arbitrary key list: 1-2, foo | bar

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
Copy link

carlosame commented May 8, 2020

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
Copy link

carlosame commented May 8, 2020

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.

@jonathantneal
Copy link
Contributor Author

jonathantneal commented Oct 27, 2020

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

Hey @LeaVerou, in your example, did you intend to reference a quality of the element? I’m unsure at the moment how the above example (50vw < 400px) would do this, but in your example (100% < 50vw), I’d presumed the 100% referred to the element width. However, I was talking with @bkardell earlier today, and we realized we didn’t agree on what we thought you meant. 🤷 I think Brian is up to things related to this, and we’d all want the ideas represented as the authors intended them. 😃

@LeaVerou
Copy link
Member

LeaVerou commented Oct 27, 2020

@jonathantneal 100% would refer to whatever percentages resolve to in the property. If the property is margin-left, indeed they resolve relative to element width.

@tabatkins
Copy link
Member

tabatkins commented Oct 27, 2020

Yeah, cond() is basically a "math function", interpreting its values in the same way and with the same timing as any other math function, so 100% is interpreted identically to how it would be in a calc().

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Feb 23, 2021

Based on my understanding of cond() and switch(), I'm not yet convinced that they are distinct. But maybe I am also missing something. 😅

  • calc()/min() etc are math functions, validated at parse time
  • var() requires invalid-at-computed-value-time behavior
  • still… we're allowed to use a var() inside a calc()

In a similar way, it seems to me that the var-like behavior and the property-limitations of the switch proposal are inherent to the value being queried, not the function itself. Based on my conversations with @bkardell, it sounds like the proposed available-inline-size query has certain restrictions, but other future query values might hook into other phases of the rendering lifecycle, with different constraints.

It also seems to me that cond((100% > 500px) …) and switch((available-inline-size > 500px) …) would usually mean the same thing. The only difference I see is that 100% takes different meanings in different properties, where available-inline-size always means the same thing (but only available in certain properties).

Is it possible to separate the proposed functions (eg cond() or switch()) from the element-query values (eg available-inline-size), and think of those as distinct features that can be used together in certain ways? Could we have a joined cond/switch function with math-like behavior, and then also provide var-like lifecycle-values with their own limitations-per-property?

@tabatkins
Copy link
Member

tabatkins commented Feb 24, 2021

There's nothing special about "var() in calc()" - it's identical to var() anywhere else, and makes the property subject to IACVT.

The point is that calc() (and math functions in general) do not do that - they're exactly like any other ordinary value. There's no significant difference, functionality-wise, between width: 10% and width: calc(10% + 2px), or any other math expression. Both of them are validated at parse-time (and so can be thrown out if invalid), and calc() doesn't introduce any dependencies between properties beyond what units themselves do.

It also seems to me that cond((100% > 500px) …) and switch((available-inline-size > 500px) …) would usually mean the same thing.

"Usually the same thing" and "exactly the same thing" are a very wide gulf here. ^_^ The cond() is exactly as troublesome as a calc() would be - width: calc(100% + 50px) and width: cond(100% > 500px; 20px; 30px) both invoke the exact same machinery at the exact same time, and as far as the layout algorithms are concerned, they're identical - they're both functions that (in this case) involve a percentage, so in certain situations they'll behave as auto, in others they'll initially resolve the % to 0 and later resolve it against the true size, etc. No fundamentally new machinery is involved.

Compare with width: switch((available-inline-size > 500px) ...). available-inline-size should always resolve to a correct value; it's specifically meant to work in the kinds of situations that %s don't work in. It resolves "late" and causes its further internal layout to pause for it, until it knows for sure what its available inline size is.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Feb 24, 2021

Right. Those were exactly my points. :)

And I think they back up my argument that we can consider the switch() function & the available-inline-size value as two distinct features that work together – exactly like calc() and var() work today:

  1. A math-like function that is entirely identical to cond() and works on ordinary values without IACVT
  2. New values like available-inline-size which would be useful in a function like that (and maybe also on their own?), but would trigger IACVT

One function can handle both switch() and cond() use-cases, depending what values are used for comparison. The need for IACVT is tied to the values, not to the function itself.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Feb 24, 2021

Notes from an offline conversation with Tab:

  • It's the <any-value> output, that currently makes the switch() function trigger IACVT fallback behavior
  • cond() avoids that by limiting output to single <number> values
  • That distinction goes out the window if a var() is used in the cond() condition or its output value, triggering IACVT anyway

Which means that cond() provides a subset of switch() use-cases – and only a subset of those cases would allow parse-time fallbacks (avoiding IACVT). That sub-subset (ordinary number input & output) is the only use-case where the two functions differ.

I'm not convinced that distinction warrants shipping two otherwise-identical-looking functions. We would need to teach authors that yes, switch() can handle all the use-cases, but in very particular circumstances cond() might provide a nicer fallback path, as long as you never put a variable inside it. I don't believe IACVT is worth that much effort to avoid.

Of course @LeaVerou's initial proposal was based on a much simpler ternary if(<condition>, <true>, <false>)… Simplifying the math function that dramatically, it might provide a more distinct/useful shortcut. That proposal makes more sense to me than the middle-ground cond(). Still, it's not much simpler than using switch as a ternary:

.example {
  gap: if(100% > 500px, 2em, 1em);
  gap: switch((100% > 500px) 2em; 1em);
}

@LeaVerou
Copy link
Member

LeaVerou commented Feb 25, 2021

Agreed with Miriam. Also, I know these names are pre-bikeshedding, but just saying, I think switch() is a fairly confusing name to non-programmers. And I'd avoid abbreviations like cond().

Of course @LeaVerou's initial proposal was based on a much simpler ternary if(<condition>, <true>, <false>)… Simplifying the math function that dramatically, it might provide a more distinct/useful shortcut. That proposal makes more sense to me than the middle-ground cond(). Still, it's not much simpler than using switch as a ternary:

.example {
  gap: if(100% > 500px, 2em, 1em);
  gap: switch((100% > 500px) 2em; 1em);
}

Just here to mention that I started an unofficial draft about this here: https://drafts.csswg.org/css-conditional-values-1/#if
It's very very very rough right now, and I would really welcome feedback for fleshing this out further. Especially from @tabatkins who has specced all the other math functions.

I've just re-read the entire thread, and I can't fully understand the difference between if(), cond() and switch() besides allowing for "else if" values, which is syntax sugar. Is it that switch() accepts MQ-style conditions?
If so, we don't need three different functions for expressing conditionals, once we have a <condition> value, we can have different syntaxes/functions that return <condition> to be used in if(). So e.g. we could have a media() function that accepts a media query and returns a <condition> which can be used anywhere a <condition> is accepted. And later a container() functions for returning <condition>s based on container queries. Or even an supports() function for terser @supports. We don't want separate if() functions for every type of condition we may want to specify, do we?
Also, just like any other value type, conditions can be assigned to variables, to avoid repeating lengthy conditions in multiple places, which is another problem that was mentioned in the thread.

@tabatkins
Copy link
Member

tabatkins commented Feb 25, 2021

cond() is a math function, so it can be parse-time verified, and because its output is limited to math values, it can have a useful TypedOM representation. switch() is a var-like function, so it must be assumed to be valid at parse time, triggering IACVT if not, and its output is freeform CSS, so all it can offer in TypedOM is a CSSUnparsedValue.

nth-value() is functionally identical to switch(), but is a shortcut for the simple case of just wanting to select from a list of N values. It's intended to let you heavily use MQs without having to shard your styles
(duplicating selectors, etc) across multiple MQ blocks; instead, you can use the MQs to set a single custom property, then write your styles once in your normal stylesheet and use nth-value() to select which one gets activated.

@faceless2
Copy link

faceless2 commented Feb 25, 2021

A definite +1 to a math function, in the sense that it's evaluated at the same time and way as calc() - that way I'd expect it to be very easy to implement.

I'm wondering if it's necessary to limit arguments 2 & 3 to math-type values? Clearly that's how it will be used most of the time - really all the comments but #5009 (comment) are considering this general issue for lengths or numbers.

Math functions can already be integers, lengths, angles etc, and if the type is invalid where used, the function itself is invalid. I realise it might mean changes, but I can't see any reason why extending that to strings and colors, for example, would be impossible. I'm not entirely convinced it would be useful, but that's to be determined.

Trying to imagine some cases where wider types would be used, but the only ones I can come up with are pretty contrived:

#phone-number {
    background-color: if(9ch > 200px, #F00, #FFF);
}
.greeked {
    font-family: if(1em < 6px, "greeked", "sans-serif");
}

Although I have a sense of foreboding about that last one as it would change the values for "ex", possibly leading to a loop. Maybe I'm talking myself out of this, but still worth considering before they're ruled out.

(and if this the bit where we bikeshed names then I'd opt for if(5em<200px, 2em, 1em) over cond())

@LeaVerou
Copy link
Member

LeaVerou commented Feb 25, 2021

Oh absolutely, <any-value> is a must for anything like this, in fact I have examples with keywords and <image> values in my proposal.
In fact, I'm not even sure about the comma-based syntax because that makes it impossible to specify values that include commas, so an alternate syntax I'm considering is keyword-based:

flex-flow: if(100vw > 500px then row else column);

@faceless2 Note that in your second example, these are keywords, not strings, so they wouldn't have quotes.

@faceless2
Copy link

faceless2 commented Feb 25, 2021

Whoops. They're definitely supposed to be strings, but "sans-serif" was the wrong choice - let's pretend I wrote "Arial" there. But actually I'm not sure about substituting <any-value> myself, because of the difficult typing it - that's why I'd stuck with strings and colors. The same issue applies for attr() and general tokens are excluded there for the same reason.

A nonsensical example to illustrate this is: font: 12px if(100vw > 500px, bold, serif) sans-serif. You can't evaluate any of this at parse time, which is where calc() is evaluated. So while I love the concept and the flexibility if offers, it's a totally different mechanism to the "math-like" version of the solution - the only solution I have an opinion on at this point, because it's so simple to do.

Although I did finally think of a useful example that meets this criteria - layout dependent background images.

background-image: if(width < 100px, url("small.png"), url("large.png"));

@tabatkins
Copy link
Member

tabatkins commented Feb 25, 2021

I'm wondering if it's necessary to limit arguments 2 & 3 to math-type values? Clearly that's how it will be used most of the time - really all the comments but #5009 (comment) are considering this general issue for lengths or numbers.

In theory, no, we could widen the output type somewhat. In practice, that edges closer and closer to having to do combinatorial syntax-checking across all the possible outputs to make sure the property is valid at parse-time. If we want to avoid that, we need to have the possible output types be statically knowable just from the function itself, with no outside context. Colors might be okay, as their syntax doesn't overlap with math expressions at all, but going much wider than (particularly to arbitrary keywords) is probably out of the question. At that point we're dealing with <any-value> and getting var-like IACVT behavior instead parse-time invalidation. And I'm loathe to require authors to do relatively complex type recognition to know which function they're allowed to use; I'd rather have a very simple "is it a number? then you can use cond; otherwise you must use switch" rule.

Alternately, a suite of specialized functions, with the output type baked into the name, would work - if-color(), if-numeric(), if-url(), etc. That still wouldn't give us arbitrary keywords, tho.

@LeaVerou
Copy link
Member

LeaVerou commented Feb 25, 2021

@faceless2 Yeah, it would need to have IACVT behavior just like var(). Are there any use cases where that is a problem?

@tabatkins
Copy link
Member

tabatkins commented Feb 25, 2021

Losing the ability to do parse-time fallback, and thus the ability to future-proof your code by providing older, more widely-supported variants, isn't something we should give up lightly. It's been vital to allowing CSS to safely evolve over the years. IACVT behavior was introduced to var() as an unfortunate compromise.

For @faceless2's example, for instance, they couldn't use src() in the function and provide a fallback url() for older browsers.

Also note my comment in #5009 (comment); if the output type is var-like than that means Typed OM can't offer any better representation for the output values than CSSUnparsedValue. cond(), on the other hand, can actually provide the output types as a CSSNumericValue, because it knows what type they are.

@LeaVerou
Copy link
Member

LeaVerou commented Feb 25, 2021

The ability for parse-time fallback was far more important in the past before @supports.
Today, given the increasing popularity of custom properties, it's becoming harder and harder to rely on it anyway.
Note that for @faceless2's example cond() still wouldn't work (if I understand it correctly), since src() and url() are not the kinds of simple math values that this would need to be restricted to.

Also note my comment in #5009 (comment); if the output type is var-like than that means Typed OM can't offer any better representation for the output values than CSSUnparsedValue. cond(), on the other hand, can actually provide the output types as a CSSNumericValue, because it knows what type they are.

Why is that? I thought Typed OM returned computed value time representations? If not, there should be something that does.

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Feb 25, 2021

I agree with @LeaVerou that var-like behavior is not a problem for authors, even if it was an unfortunate compromise, and that many use-cases (flexbox & grid-templates stand out) rely on any-value output. Parse-time fallback is great, but we also have @supports. I do not believe that authors will spend the time to distinguish between two seemingly-identical functions just to get parse-time fallback in a subset of use-cases. Instead we'd be introducing non-obvious failures, any time an author without full IACVT knowledge (most authors) adds a var() inside an existing cond(), and suddenly breaks the fallback. Using a math function is not a stable promise that you get parse-time fallback. As a teacher, I would always encourage people to use @supports anyway, for either function, just in case.

I like where Lea is going with the if() proposal, designed to handle a wide range of condition-types and outputs.

@tabatkins
Copy link
Member

tabatkins commented Feb 25, 2021

I thought Typed OM returned computed value time representations? If not, there should be something that does.

When you get a computed value, yes. You get also get specified values from the OM! As well, anything with %s probably isn't resolving at computed-value time; those are used-value time.

Using a math function is not a stable promise that you get parse-time fallback.

This argument applies equally well to any syntax - using rgb() is not a stable promise that you get parse-time fallback either, in that case.

As a teacher, I would always encourage people to use @supports anyway, for either function, just in case.

This is a pretty reasonable argument, tho.

@joeshub
Copy link

joeshub commented Feb 25, 2021

+1 for this array syntax. I have really enjoyed using it already in styled system

@mirisuzanne
Copy link
Contributor

mirisuzanne commented Feb 25, 2021

This argument applies equally well to any syntax - using rgb() is not a stable promise that you get parse-time fallback either, in that case.

Exactly. :) My point was that the IACVT ship has sailed at this point, and impacts any CSS that might use variables. No one can rely on any function to consistently have parse-time fallback.

@matthew-dean
Copy link

matthew-dean commented Jun 16, 2021

An if()-style function is extremely useful in having a declarative way to choose options. It's how Less libraries embed a lot of conditional logic without more verbose mixin or @if at-rule-like constructs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests