Currently, custom properties can be used to hold small pieces of data to be used as parts of larger values. Despite being called custom properties, they are mainly used as variables. High-level custom properties that control a number of other CSS properties cannot be implemented.
Besides limiting regular CSS authors, this makes it impossible for custom element authors to follow the TAG guideline to avoid presentational attributes and to use a custom property instead, except for very simple bits of data like fonts, colors, and lengths. For anything more complex, web component authors use attributes instead.
Examples from a variety of custom element libraries:
placement
attributesize
attribute in Shoelace and in Spectrumpill
attribute orientation
attributeleft
attributeplacement
, offset
, tip
attributesappearance
attributeI can collect more if needed, examples abound in nearly all component libraries. Currently, these are impossible to implement as CSS custom properties, for a number of reasons:
pill=on
vs border-radius: 999px
).Essentially, component authors need more high-level custom properties that encapsulate the corresponding declarations better instead of just containing fragments of values.
Some proposals to address this problem focus on a JS-based way to monitor property changes [w3c/webcomponents#856], but that appears to be hard to implement. So, I'm wondering if we can address this from CSS instead, especially since it would also address a number of other use cases too that are unrelated to components, a big one being mixins, without the problems that we had with @apply
.
There are discussions in the group about inline conditionals [#4731, #5009]. If we were to have such conditionals, these would be possible to implement, but very painful (each declaration value would need to be one or more if()
). I was wondering if we could simplify this.
A pseudo-class such as :if-var(<dashed-ident> <comparison-operator> <value>)
would solve this ideally, but would likely not be implementable due to cycles. OTOH we already do cycle detection for variables, so perhaps it is? If so, I can flesh out a proposal.
Otherwise, perhaps a nested @rule
?
my-input {
@if-var(--pill = on) {
border-radius: 999px;
}
}
With nesting, that would even allow multiple rules, so it would cater for use cases such as e.g. the tabs placement without too much repetition.
One way to implement this would be as sugar for multiple if()
s, but that would have the undesirable side effect of all containing declarations being set to initial
when the conditional doesn't match and there's no @else
, which is suboptimal.
Whatever solution we come up with, some things to consider:
--size: [small | medium | large]
) or it may be used directly in some values, and also in conditionals.I love the idea here because the conditions may be created _and_ referenced from within CSS.
This shares _some_ thematic similarity with the custom :state()
pseudo-class. They both help authors style by custom conditions, and both without being directly reflected in serialized HTML. While :state()
differs in by using selectors and not supporting assignment within CSS, Iām sharing it so we can glean from their similarities and from feedback on :state()
, as that one is already _shipping_ behind a flag in Chrome at the time of this writing.
I think we have to be careful with the naming of the expression, independent of how it's actually implemented. I like the approach of the @if-var but I really don't like the naming, as we don't know if we may expect constants or anything other than variables in CSS in the near or distant future.
Thinking about it I'd like to have a universally @if
that could be used with all types of values not only with custom properties. That way we could use it with other already existing stuff like env()
.
e.g.
.my-input {
@if(var(--pill) = on) {
border-radius: 999px;
}
}
.my-input {
@if(env(safe-area-inset-left) = 20) {
border-radius: 10px;
}
}
Thought even further it would be possible to redefine already existing properties. Like _background_ kinda did for _background-color_. Imagine exposing the screen width or anything to the user agent with like env(width)
.
That way it would be possible to combine conditions of each kind and even write media queries and media conditions as part of @if
conditions.
e.g.
.my-input {
@if(env(width) > 768) {
border-radius: 10px;
}
}
.my-input {
@if(env(width) > 768) and (var(--pill) = on) {
border-radius: 999px;
}
}
@Que-tin Naming is typically decided at the end, all features are proposed with an implied "name to be bikeshedded".
The problem with such a generic conditional is ā as usual ā cycles.
Consider this:
.my-input {
@if (env(width) > 768px) {
width: 700px;
}
}
Also, with such generic expressions, there are ambiguities. E.g. what does @if (calc(env(width) + env(height)) > 100%)
mean, since percentages mean different things in each property?
A pseudo-class such as
:if-var(<dashed-ident> <comparison-operator> <value>)
would solve this ideally, but would likely not be implementable due to cycles. OTOH we already do cycle detection for variables, so perhaps it is? If so, I can flesh out a proposal.
It would be good to hear from implementors about this, and in general solicit feedback about a possible way forwards that is implementable and covers most use cases, so I'm gonna go ahead and Agenda+ this.
Note to chairs: I cannot attend the APAC call, so this would need to be next week.
Good point. There are actually a few exceptions to make at this point, hence percentages as well as integers, should have no meaning at all in this case (seen relative to the element the condition is used in). It would be hard to consider when values are relative and when absolute to the element they are used in inside of the if condition.
The advantages in my example above would actually be that you could use media conditions (if implemented as e.g. env(width)
in the user agent) as part of the CSS properties as already know in e.g. SASS.
It would also be possible to use things like attr() in the future.
my-input {
border-radius: 0px;
@if-var(--pill = on) {
border-radius: 999px;
}
}
#my-input {
--pill: off;
}
@LeaVerou Just to make sure I understand the proposal, for <my-input id=my-input>
you'd expect a border-radius
of 0px
, right?
(... interpreting that upvote as a "yes"):
Having selector matching (@if-var
is not very different from a nested :if-var
) depend on computed-value-time things adds far too much complexity, even if it's not impossible.
IMO if we want to if-else on custom properties, then that evaluation needs to happen computed value time, i.e. if-else needs to be part of the value. Or we need something which translates into effectively doing that internally. _Or_, we add a new kind of custom property which can be known selector matching time.
I agree with @andruud fwiw. I don't understand why something like this:
There are discussions in the group about inline conditionals [#4731, #5009]. If we were to have such conditionals, these would be possible to implement, but very painful (each declaration value would need to be one or more if()). I was wondering if we could simplify this.
Is really more painful / complex than what's being proposed here.
Basically, something like border-radius: if(var(--pill), 0px, 999px);
seems simpler and much less action-at-a-distance to me.
In fact if you make --pill
a boolean variable with values 0
or 1
, you can already accomplish this particular usecase (border-radius: calc(var(--pill) * 999px)
), though I understand there are more complex use cases that aren't probably covered by such a thing (and also it is a bit more obscure than some more explicit syntax).
@emilio --pill
might not be a good example, as it only needs to control one property, so indeed one if()
serves that case just fine. However many of them need to control multiple, often across several rules. I listed a lot more in my original post. Also, once you have several of these custom properties controlling intersecting sets of properties, solving it with if()
suffers from combinatorial explosion. Lastly, with if()
, you need to provide an alternative (either explicitly or implicitly), whereas a lot of these are about additional "traits" that only set certain declarations when they are actually specified.
Idea: Would it be easier to implement if one was only able to set other custom properties in these conditionals?
The CSS Working Group just discussed [css-variables?] Higher level custom properties that control multiple declarations
.
The full IRC log of that discussion
<dael> Topic: [css-variables?] Higher level custom properties that control multiple declarations
<dael> github: https://github.com/w3c/csswg-drafts/issues/5624
<dael> leaverou: There is a very reasonable tag guideline that custom elements shoudl use properties for presentational elements. With current state of custom properties this is impossible for non-trivial
<dael> leaverou: Current custom prop can only be literal fragments and you need to transform
<dael> leaverou: There are problems where we need to add inline conditionals.
<dael> leaverou: However, when you have lots of these properties intersecting then it can get really messy if only 2 you have is inline funcitons that need both condisions.
<dael> leaverou: Ideal is something to cascade but not sure feasible. Wanted impl feedback and then I can draft a more detailed proposal
<dael> leaverou: Examples I've looks at from component liberties are in the issue
<dael> leaverou: Some impl have weighed in in the issue [missed]
<dael> leaverou: Wondering if set of constraints could be introduced to make it more feasable. Wanted to bring to attention of group for more ideas or thoughts
<dael> astearns: Feedback on the shape of the feature?
<dael> fremy: I think it's a pretty good idea.
<dael> fremy: Really something that's a limitation of custom properties. Quite true when you use attributes you do more than reuse variable.
<dael> fremy: Pseudo class you can't use in theory but it would be really nice to have syntactic sugar. I know you can do it meta languages. Good to auto-prefix with an if condition. That would give us most of advenatages. Extend the css selector syntax to have an id for all properties
<TabAtkins> Assuming we're fine with simple conditionals in an if() function (which we've discussed before and this should be okay), doing an at-rule inside holding a block of props could have a reasonable desugaring to that.
<dael> leaverou: One thing to keep in mind is these often need to control multiple elements in the component. Alignment might need to control margins and padding. Ideally should work
<dael> leaverou: [missed]
<TabAtkins> It would have some side-effects - any properties in the block are effectively using a variable, so it would kick in IACVT behavior, etc.
<dael> leaverou: Often they need to control properties in mutli elements. Example alignment controls spaces, padding, etc in multi child elements. Good to keep in mind that it plays nicely with nesting module
<dael> TabAtkins: invalid at computed value time = iacvt
<fremy> @TabAtkins: I would think it solves many issues
<dael> leaverou: Some reasonable syntax to combine and falling back to invalid at computed value time would prob be acceptable
<leaverou> As long as we can combine conditions and nest them, having IACVT as the ultimate fallback is acceptable
<TabAtkins> so like .foo { color: blue; @if (var(--state) = one) { color: red; } }
, it'd desugar to .foo { color: cond(var(--state) = one, red; blue); }
<dael> astearns: I think this is a really interesting proposal and I'd like to see further discussion on what we can do here. Any major concerns about spending time on this?
<dael> astearns: I think we should take this back to the issue and/or come up with a proposal which we can file issues on. IT's a really good idea. Anything else from group?
<dael> leaverou: I primarily wanted to draw impl attention. I can't design impl needs. Continue on issue is fine
As mentioned above, I would like this to be pursued further. That sounds very useful (but even having if
would be nice, I think).
Random thought: another possible syntax for the syntactic sugar:
selector {
property-one: value;
@transform-values if(--condition: true, $);
property-two: value-x, value-y;
property-three: value-u, value-v;
}
In the CSSOM, the @transform-values wouldn't be reflected, it would be a transform applied while parsing declarations.
selector {
property-one: value;
property-two: if(--condition: true, value-x, value-y);
property-three: if(--condition: true, value-u, value-v);
}
So we'd previously discussed the cond()
function as one of the "switch" variants, which would have effectively these exact semantics but grouped differently. That is, given Anders' example (lightly edited):
my-input {
border-radius: 0px;
@if (var(--pill) = on) {
border-radius: 999px;
}
}
it would be equivalent to the following using cond()
:
my-input {
border-radius: cond((var(--pill) = on) 999px; 0px);
}
(I wrote cond()
as being essentially a math function, but simple equivalence for keywords seems reasonable to mix into here. These might want to use additional non-math comparators, like "is the value in this set", so we'd have to give it some thought and care.)
The benefit of the at-rule syntax is that it inverts the grouping - when you have a bunch of variants of several properties, the at-rule groups them by variant, while cond()
groups them by property. Which is more readable varies case-by-case, but when it matters it can have a large effect on the readability, particularly when the variants don't all affect the exact same set of properties. Noticing that sort of variation can be very hard when looking across several cond()
functions.
If we treat the two as exactly equivalent, just sugar variations of each other, then this does end up implying some slightly non-obvious behavior in some cases. For example, in:
my-input {
@if (var(--pill) = on) {
color: green;
}
}
(Note the lack of "default" color
in the block.) Then if --pill
is off
or whatever, color
wouldn't just be not set, it would be IACVT and end up setting itself to inherit
, which has some minor cascading implications. (This is presumably the behavior we'd define for cond()
if you don't provide a default clause.)
I don't think this is a big deal, it's just worth understanding the implications. I think this is much better than defining this as an almost identical feature that actually works completely differently under the covers.
Regarding Tab's proposal, we can do slightly better, and copy the value preceding the @if
as a fallback:
my-input {
color: blue;
@if (var(--pill) = on) {
color: green;
}
}
would be either blue
or green
, by filling the fallback part of the cond
with the already-existing value in the declaration (and only in the declaration, not across another selector, so that would still be a limitation, just a more convenient one because it allows to specify a default.
Yes, that exactly what the first part of my post was implying - you'd collect a given property across all the if-blocks and "plain", and group them into a cond() (with the "plain" version being the final default branch of the cond()).
I think this is a great idea and would find it very useful in our applications as we author them today.
We might author a web component that provides an interface to change the border radius
of part=foo
through var(--border-radius)
.
I'd assume through a conditional, we can expose an additional interface that would inherit a custom property from an ancestor.
@if(conditional: true) {
--prop: var(--new-prop);
}
Based on the conditional, a component we author might take the shape of the following:
<your-element>
#shadow-root
<style>
[part=foo] {
border-radius: var(--border-radius, 0);
@if(--theme: bubbles) {
/* inherits --app-border-radius from ancestor such as :root */
--border-radius: var(--app-border-radius, 15px);
}
}
</style>
<div part="foo">Text</div>
</your-element>
This would give our customers to control the behavior of --theme: bubbles
outside of the component they don't own.
Since CSS custom properties inherit through shadow trees, my assumption is a customer can gain better control by changing CSS contextually in their component.
Their app might define 30px
for all border-radius
.
:root {
--theme: bubbles;
@if(--theme: bubbles) {
--app-border-radius: 30px;
}
}
But contextually, they want to target the prop from ::part(foo)
and override the applications border-radius
of 30px
to be 4px
for the instance of <my-element>
.
<my-element>
#shadow-root
<style>
::part(foo) {
@if(--theme: bubbles) {
--border-radius: 4px;
}
}
</style>
<!-- customer does not own this component -->
<your-element></your-element>
</my-element>
I highlight this example to capture how Salesforce would share styles between components but expose additional control for our customers.
@tabatkins How will nested conditions be written?
I saw this example of yours in the linked issue of course I think commas make more sense here.
margin-left: cond((50vw < 400px) 2em, (50vw < 800px) 1em, 0px);
But what if I turn the cond around? This makes it quite unreadable in my opinion especially the comma-separated list of possible values. Imagine having more than two conditions nested in each other
margin-left: cond((50vw < 400px) (50vw < 800px) 1em, 0px, 2em);
Or will it be possible to do smth like this to increase readability? Should be, correct?
margin-left: cond((50vw < 400px) ((50vw < 800px) 1em, 0px), 2em);
But how about the at-rule?
my-input {
border-radius: 0;
@if (var(--pill) = on) {
@if (var(--half) = on) {
border-radius: 10px;
}
border-radius: 999px
}
}
Or will it be smth like this:
my-input {
border-radius: 0;
@if (var(--pill) = on) {
border-radius: 999px
}
@if ((var(--pill) = on) (var(--half) = on) {
border-radius: 10px;
}
}
May it be possible to also implement some kind of logical operators for the conditions? AND, OR and NOT would be amazing to have in both solutions. I mean, of course, it would be possible to achieve this even without the operators, it just would increase the readability a lot.
my-input {
border-radius: cond((var(--pill) = on) and (var(--round) = on) 999px; 0px);
}
my-input {
border-radius: 0;
@if (var(--pill) = on) and (var(--round) = on)) {
border-radius: 999px
}
}
I think this is one of the most complex proposals up to date as there are so many different approaches and solutions as well as things that have to be paid attention to.
IACVT could work for components, since presumably all their styles are defined in the same place. Assuming these rules can be nested with predictable results, and do take all values defined in the rule into account before triggering IACVT (per @FremyCompany and @brandonferrua's suggestions), I don't think the downsides of IACVT will come into play too frequently. However, especially in that case, nesting support becomes really important.
I love the idea of using var()
instead of <dashed-ident>
so we can compare any two expressions, not just variables. E.g. this opens the door for conditionals like 1em < 16px
, so it's a strictly more powerful feature. We should probably define these comparison expressions separately, in Values & Units, and reference them in any other spec that needs them.
Indeed, not
, or
, and and
operators should certainly be allowed, most likely with mandatory parentheses like in @supports
.
A few issues with speccing generic comparison expressions: how are certain values interpreted outside of a declaration context? E.g. what are percentages relative to? If we disallow percentages, then I suppose we should define this as a union of specific types (and both sides of the comparison need to be of the same type), i.e. <dimension> | <color> | <image> ...
. There is currentcolor
that works differently in color
, and em
that works differently in font-size
. What do we do with them? Are there other values that are ambiguous outside of a declaration context? So far the only values we've allowed outside of declarations are those permitted in media features, which I think are limited to lengths.
Also, I suppose anything that is not a <number>
or <dimension>
would only work with equality (and inequality, if we define one, we could also just depend on not
for that).
Is equality based on serialization of used values? E.g. is #f06
equal to #ff0066
? What about to rgb(255 0 102)
? They all serialize the same.
(this is mostly to @tabatkins but any input is welcome)
Edit: Oh, actually, if we define this as just sugar for inline if()
, then we can use any value and it's interpreted in the context of each property inside the block. The equality question still stands.
Hi,
I'm new here but I would like to give feedback on this proposal that seems a powerful way to achieve many things in CSS. I would also like to stress to participants that CSS improvements can be used in other areas than custom components, and those use cases should be considered as well.
There is a number of features that I find interesting in the @if
proposal:
cond()
) which allows to avoid defining a default value for the properties that are set@apply
spec which is now abandonned.For example, here is how I would use it for mixins:
* {
@if(var(--mixin-danger) = on) {
background-color: red;
}
}
#my-error-message {
--mixin-danger: on
}
As for naming, I would not use @if
but rather @when
because @if
calls for a @else
and @elseif
which can complexify the syntax (elseif condition implicitly contains a negation of the conditions defined before, I'd prefer it to be explicit).
Also, would such condionals be allowed to be nested ?
Edit: Oh, actually, if we define this as just sugar for inline if(), then we can use any value and it's interpreted in the context of each property inside the block. The equality question still stands.
This would bring surprising behaviour where a property in your conditional could be applied but not the next one because the conditional applies differently to it.
I think the sanest approach would be to compare string equality and only allow comparison on types that are independent of the context. If you define --color1: #f00
and --color2: red
, then @if var(--color1) = var(--color2)
would be false. If we want to be able to compare colors, then perhaps we can have a function that would take a color expression of any form and return a standardized value that can then be compared.
If you allow colors to be compared, then you'll get a problem when a custom property contains something that can be interpreted as a color but is not meant to be a color. it could have an equality match with a value where this is not expected.
@que-tin
@tabatkins How will nested conditions be written?
I saw this example of yours in the linked issue of course I think commas make more sense here.
margin-left: cond((50vw < 400px) 2em, (50vw < 800px) 1em, 0px);
But what if I turn the cond around? This makes it quite unreadable in my opinion especially the comma-separated list of possible values. Imagine having more than two conditions nested in each other
margin-left: cond((50vw < 400px) (50vw < 800px) 1em, 0px, 2em);
Or will it be possible to do smth like this to increase readability? Should be, correct?
margin-left: cond((50vw < 400px) ((50vw < 800px) 1em, 0px), 2em);
I'm not @tabatkins but hopefully your question is directed to the group and not specifically towards Tab?
Parenthesizing the conditional like that makes for a very hard to read syntax any way you order these.
I would argue that a comma-separated three argument function is the way to go:
margin-left: if(50vw < 400px, 2em, if(50vw < 800px, 1em, 0px));
which I believe is more readable than any of the examples above, and more externally consistent (this is how inline conditionals work in CSS preprocessors (Sass Less), as well as in spreadsheets).
But how about the at-rule?
my-input { border-radius: 0; @if (var(--pill) = on) { @if (var(--half) = on) { border-radius: 10px; } border-radius: 999px } }
Nested @if
rules definitely need to be allowed and desugar via inline conditionals using and
. However, your example raises an interesting point: border-radius: 999px
comes after the @if (var(--half) = on)
, so it would be reasonable to override border-radius
regardless of the value of var(--half)
and desugar to:
my-input {
border-radius: if(var(--pill) = on, 999px, 0);
}
I think what you meant to write was perhaps this:
my-input {
border-radius: 0;
@if (var(--pill) = on) {
border-radius: 999px;
@if (var(--half) = on) {
border-radius: 10px;
}
}
}
which would desugar to:
my-input {
border-radius: if(var(--pill) = on, if(var(--half) = on, 10px, 999px), 0);
}
@mildred: Mixins was one of the use cases I mentioned in the original proposal, however do note that implementing these as sugar on the if()
+ the IACVT behavior both limit their utility for those use cases quite severely. Namely, in your example, background-color
would be set on every element, either to red
, or to unset
. It would be essentially equivalent to this:
* {
background-color: if (var(--mixin-danger) = on), red, unset);
}
#my-error-message {
--mixin-danger: on;
}
This means that if you have something like this:
* {
@if(var(--mixin-danger) = on) {
background-color: red;
}
}
div {
background-color: yellow;
}
#my-error-message {
--mixin-danger: on
}
and a <div id="my-error-message">
, it wouldn't be red
, but yellow
, because background-color
does not contain a conditional anymore. However, even if @if
worked differently and actually cascaded, background-color: yellow
would still override it. Basically, you'd need to stop using non-custom properties outside of @if
rules to get these to behave as mixins.
Unfortunately, the feedback we got from implementers is that if we make the rule cascade, it is much harder to implement. I'm unclear on whether there are any constraints that would make cascading conditionals implementable (either limitations in what they contain, or in the condition itself). The question is, if they do not cascade (outside the rule they are defined in), do they still solve a large number of use cases?
Trying to write up an Unofficial Draft on this, I've come across a few issues. @tabatkins and I had a good discussion yesterday about them. I'm going to try and summarize the current status here.
@if
Pseudo-classes are out of the question, since they match at a completely different point and would cause architectural issues otherwise.
If we make @if
cascade, we'd need to carry invisible extra context with each property, which is a significant increase in complexity across every declaration, whether it's inside a conditional or not.
It looks like the best tradeoff of implementation convenience and use case coverage is to implement @if
based on my original idea of desugaring it into inline if()
calls, but also taking into account any properties defined within the same rule as the @if
block. E.g. this:
.button {
border-radius: 2px;
@if (var(--pill) = on) {
border-radius: 999px;
padding: 0 1em;
}
}
desugars to:
.button {
border-radius: if(var(--pill) = on, 999px, 2px);
padding: if(var(--pill) = on, 0 1em, unset);
}
There are certain values in CSS that evaluate differently depending on which property they are specified on. The obvious one is percentages, but also em
, rem
, lh
, rlh
, currentColor
.
Currently, for a generic inline if()
function, it makes sense to evaluate the condition in the context of the property it's specified on. However, this means that if we desugar @if
by using if()
on each value for each declaration it contains, any relative values used may make the condition true for some declarations and false for others. E.g. consider this:
.foo {
@if (1em > 5%) {
width: 400px;
height: 300px;
}
}
which desugars to:
.foo {
width: if(1em > 5%, 400px);
height: if(1em > 5%, 300px);
}
Now consider that an element that matches .foo
is inside a 600px
by 400px
container and has a computed font-size of 25px
; This makes 1em > 5%
evaluate to false
on the width
property and true
on the height
property, which would make the @if
partially applied. We most definitely don't want that.
One solution we came up with was to define two kinds of inline conditional functions: One that works as described above, and is not used for desugaring @if
, and one whose main purpose is for desugaring @if
, let's call it property-agnostic-if()
(name obviously TBB) for the purposes of this discussion. That function will evaluate each type within its condition against a predefined property, e.g. color
for <color>
values, width
for <length>
or <percentage>
, font-size
for em
and so on. This means it will evaluate the same on any property, preventing partial application for @if
blocks that do not contain nested rules.
However, once CSS Nesting comes into play, partial application becomes a problem again. Many use cases require the @if
to control multiple rules, which would become possible with nesting. However, since condition evaluation depends on the element context, this could mean the conditional matches for the rule it's defined on and doesn't match for nested rules or vice versa! For example:
.tabs {
display: grid;
@if (var(--alignment) = top) {
grid-template-rows: auto 1fr;
& > .tab-strip {
display: flex;
}
}
}
What happens if .tabs
has --alignment: top
and .tab-strip
has --alignment: left
? So far we have not found a solution to this. Ideally, we'd want matching to happen at the root rule and everything inside the @if
block either applies or doesn't, but there doesn't seem to be any reasonable way to desugar that into inline functions. Should we just live with this and caution authors against it? Do note that for the Web Components use cases, a lot of this matching will be in Shadow DOM, which is controlled by the component author. So maybe it's ok? However, once the feature is out, authors will use it in a more general way, and nothing can put that genie back in the bottle.
Inline conditionals will have the IACVT (Invalid At Computed Value Time) behavior that we have come to know and love (?) from Custom Properties. Since @if
will desugar to inline conditionals, it will also fall back to that, which may sometimes be surprising. This means that these two snippets are not equivalent:
.notice {
background: palegoldenrod;
}
.notice {
/* Desugars to background: if(var(--warning) = on, orange, unset); */
@if (var(--warning) = on) {
background: orange;
}
}
.notice {
/* Desugars to background: if(var(--warning) = on, orange, palegoldenrod); */
background: palegoldenrod;
@if (var(--warning) = on) {
background: orange;
}
}
This also affects how CSS optimizers combine rules, since combining rules with identical selectors can now produce different effects.
There is also the example in my comment above, where even though it makes sense if you think about it, for some reason the result feels very surprising.
The CSS Working Group just discussed [css-variables?] Higher level custom properties that control multiple declarations
.
The full IRC log of that discussion
<dael> Topic: [css-variables?] Higher level custom properties that control multiple declarations
<jensimmons> Does that mean we get a CSS 2020 in 2020??
<astearns> just barely
<dael> github: https://github.com/w3c/csswg-drafts/issues/5624
<dael> leaverou: I didn't explicitly add this. WE discussed last time and didn't get resolution. Interesting discussion in issue and off GH
<leaverou> https://github.com/w3c/csswg-drafts/issues/5624#issuecomment-746339609
<dael> leaverou: I summerized current state in ^ comment
<dael> leaverou: Summary: It looks like best course of action for block conditionals. Can't use pseudo class, casuse issues. If if() cascades have to carry extra context and increases too much complexity
<dael> leaverou: Best is impl if based on idea of desugering to inline if calls and take into account properties in same rule. example in comment
<dael> leaverou: Rasises some issues b/c certain values eval differently depending on prop. Hasn't come up that much. Length in some MQs
<dael> leaverou: For example, ones we could come up with TabAtkins is %, em values, rem, lh, rlh, currentColor.
<chris> rrsagent, here
<RRSAgent> See https://www.w3.org/2020/12/16-css-irc#T17-34-13-1
<dael> leaverou: Problem. If it desugars to inline if calls nad conditional has relative values you may have cases where part of rule eval to true and a part of false. Example in comment.
<dael> leaverou: Agreed don't want partial applicaitons. How to solve?
<dael> leaverou: Came up with defining how these relative values would be evaluated. cureentColor is as if in color and so on. New inline conditional function to desugar iff
<dael> leaverou: Doesn't sound good, but couldn't come with better
<dael> leaverou: Addresses single conditional. Css nesting has same partial applicaiton problme. May have condition true for a rule but not decendnents.
<Rossen_> q?
<dael> leaverou: Might have var warning = on and a value for --warning on parent and different value on the child
<dael> leaverou: You again have @if block applied paritially
<dael> leaverou: Not sure if there's a way to address this. Couldn't come up with anything but just discussed yesterday. Don't know if there are ideas
<dael> fantasai: What do you do if content has if clause with a property that effect evaluation. if on a em and evaluate em against font size
<dael> leaverou: Can you put example in IRC?
<fantasai> @if (var(...) > 1em) { font-size: 35pt; }
<dael> leaverou: I see
<dael> leaverou: I'm not sure
<dael> leaverou: What would you suggest should happen?
<dael> leaverou: It's basically same as if you have inline if
<dael> Rossen_: In interest of time, are we ready to resolve or should we take it back to GH and continue there?
<dael> leaverou: I suppose we could go back to issue
<dael> Rossen_: Let's do that. Let's continue discussing there. I was hoping we were closer to resolution then we are. We'll come back
@LeaVerou Chances of that if
going anywhere in that particular form is slim to none due to reasons explained by @tabatkins in https://github.com/w3c/csswg-drafts/issues/3455 as he rejected that issue.
Edit: also read up on CSS mixins and the surrounding proposals related to that. The other half of this is basically that.
@isiahmeadows The one in #3455 allowed checks on _"any value"_ ā¦
[W]hat's useful about
if()
is that [ā¦] you can use it for any value.
⦠which was the reason for closing it.
This is, unfortunately, what makes this so much harder, and probably not capable of happening [ā¦]
If it the proposed if()
function were to be limited to only check a Custom Property against a certain value, then it'd be possible I guess?
/* Valid: Custom Properties */
width: if(var(--size) = big, 10em, 2em);
gap: if(var(--numchildren) > 10, 4em, 2em);
/* Invalid: Non-Custom Properties or other values */
padding: if(width > 10em, 2em, 1em);
width: if(1em < 5%, 400px, 600px);
That would immediately also solve the āPartial applicationā issue @LeaVerou mentions, as it simply wouldn't be allowed.
@bramus Okay, now that I take a closer look, I think you're right. Edit: If units are allowed in the comparison, one could define font-size: if(var(--foo) > 2em, 2em, 1.5em)
- would this produce a cycle?
@isiahmeadows font-size: if(var(--foo) > 2em, 2em, 1.5em)
won't create a cycle between "regular" properties, as the font-size
property is pointing to the custom property --foo
.
It is possible that var(--foo)
itself causes a cycle, but that's of no issue. Quoting @tabatkins in #3455 here again:
Custom properties can arbitrarily refer to each other [ā¦] and have a somewhat reasonable "just become invalid" behavior when we notice a cycle.
@isiahmeadows @bramus
I was under the impression that these days there more or less is consensus for an inline if()
/cond()
function (see the issues I linked to in my first post), so this is exploring a block form. As @bramus pointed out, the closed proposal you point to could refer to other properties etc which is way harder to implement.
I did indeed initially propose limiting conditions to comparisons between var()
references and values, but it seems that a more general form could perhaps be possible (see https://github.com/tabatkins by @tabatkins ).
Edit: also read up on CSS mixins and the surrounding proposals related to that. The other half of this is basically that.
I thought I was up to date on these, but it's entirely possible I've missed a few, were there any specific discussions you'd like me to focus on?
@bramus
Okay, now that I take a closer look, I think you're right.Edit: If units are allowed in the comparison, one could definefont-size: if(var(--foo) > 2em, 2em, 1.5em)
- would this produce a cycle?
No because em
in font-size
points to the parent font-size. Otherwise even font-size: 2em
would be a cycle š
That would immediately also solve the āPartial applicationā issue @LeaVerou mentions, as it simply wouldn't be allowed.
I don't see how.
a) Custom properties can have different values in nested rules.
b) Consider e.g. if(var(--foo) > 5%, ...)
with var(--foo)
being e.g. 12px
. Percentages are resolved differently in different properties, so the condition could be true in one property and false in another.
@LeaVerou
I don't see how.
a) Custom properties can have different values in nested rules.
They don't. Think we simply misunderstood each other here.
b) Consider e.g.
if(var(--foo) > 5%, ...)
withvar(--foo)
being e.g.12px
.
Ha, was a bit too focused there on referring to other "regular" properties vs. other custom properties. Didn't take the values they resolve to into account ā which obviously can also be em/px values, as you mention here. Ignore my statement about it resolving the partial application issue, it is incorrect.
I've been giving this issue quite some thought the past few days, and think I've come to propose an alternative syntax. It's basically a reuse of the attribute selector, but then with a custom property.
To get a bit ahead of my self, here's the pill example from the OP with this proposed syntax:
[@var(--pill)="on"] {
border-radius: 999px;
}
The motivation behind this alternative syntax is two-fold:
@if
is imperative and feels off in that way._(Note: Using nesting here to keep things tidy)_
If pill="on"
were an HTML attribute:
button, input[type="submit"] {
background-color: hotpink;
color: white;
border: 0;
&[pill="on"] {
border-radius: 999px;
}
}
Using --pill="on"
button, input[type="submit"] {
background-color: hotpink;
color: white;
border: 0;
&[@var(--pill)="on"] {
border-radius: 999px;
}
}
.square {
--width: 4em;
width: var(--width);
aspect-ratio: 1/1;
&[@var(--size)="small"] {
--width: 2em;
}
&[@var(--size)="big"] {
--width: 8em;
}
}
@var()
and attribute selectors), so there's nothing new to learn>
and <
, but limited to numbers only?_==
(comparison) instead of =
(assignment)[]
is the attribute selector@var
part should give a clue thereI don't know if this even possible _(i.e. would this all require an extra run of the parser?)_, as I don't know how CSS parsers work. Feel free to point out how flawed this is in case is shouldn't be possible ;)
@bramus There are several issues with this proposal.
var()
references. That's not a problem in itself, but if we are going to limit it that much, we may as well ditch the @var()
and just specify the <dashed-ident>
to compare against. The whole point of using var()
was that we could compare with other types of values as well (or calculations, or...)@if
that I proposed to make it easier to implement. Conceptually (if implementation was not an issue), it's trivial to resolve, the difficulty is in resolving it in a way that's also implementable. Same with nesting: we could easily resolve partial application by specifying that conditions match where they are specified, and only the result propagates to nested rules, the difficulty is in how to make that implementable.Last, I'm not sure how @if
is imperative? What makes it imperative?
Most helpful comment
@LeaVerou Just to make sure I understand the proposal, for
<my-input id=my-input>
you'd expect aborder-radius
of0px
, right?