Open LeaVerou opened 3 years ago
The CSS Working Group just discussed [css-variables?] Higher level custom properties that control multiple declarations
.
Regarding (nearly) constant third-party property values, in #6099 I proposed to use (something like) @namespace
to specify the prefix between the two dashes used within var()
, but to never use the prefix in the property on the left side of a colon.
@prefix "foo" {
--bar: green;
-foo-bar: magenta; /* unknown private property */
}
baz {
-foo-bar: maroon; /* unknown private property */
color: red;
color: var(-foo-bar); /* green */
background: green;
background: var(--bar); /* undefined custom property value */
}
baz:const(-foo-bar = "green") { /* property value is not a string but a color */
border: red thick solid;
}
baz:const(-foo-bar = green) {
border: green thick dashed;
}
baz:const(-foo-bar = #0F0) { /* color notation should be irrelevant */
border: lime thick dashed;
}
@Crissov I still feel that might get confused with vendor prefixes at least visually and for some intuitively.
@LeaVerou I'm not super thrilled about having two selector+cascade passes. Trying to come up with alternatives. Can we drop the two passes, let :const()
resolve using the regular custom properties, but make it invalid for the subject compound? E.g. you would use it like this: :const(--x:1) .thing { /* ... */ }
.
@andruud That won't cover all use cases as-is, but might cover them if modified (eg to render the entire component in descendants). Does it allow us to relax any of the other restrictions or is it just an additional restriction?
We'd relax all restrictions related to the proposed const-variables, since we wouldn't need to introduce them: :const()
would refer to the existing custom properties. When matching selectors for a given element, the computed values of the parent/ancestor/preceding-sibling is already known*, so it should be fine-ish to depend on those computed values during selector matching of a subsequent element.
Might be too annoying for authors if they have to wrap their component in a "control element" just to set high-level variables though. Perhaps combine with an inline if()
/switch()
that can be used on the element itself? (Just thinking out loud).
*
In Chrome that is. Hopefully also in other browsers.
We'd relax all restrictions related to the proposed const-variables, since we wouldn't need to introduce them:
:const()
would refer to the existing custom properties. When matching selectors for a given element, the computed values of the parent/ancestor/preceding-sibling is already known*, so it should be fine-ish to depend on those computed values during selector matching of a subsequent element.
In that case, this sounds like a much better solution over everything else that has been proposed so far.
Does that mean that we could even have full-blown conditionals, e.g. 100vw > 500px
? That would be a nice to have, though not required for the primary WC use cases.
Might be too annoying for authors if they have to wrap their component in a "control element" just to set high-level variables though. Perhaps combine with an inline
if()
/switch()
that can be used on the element itself? (Just thinking out loud).
I was hoping they'd be very useful in Shadow DOM, branching off conditionals on the main element. Yes, it would be annoying to force authors to introduce extra elements.
However, I believe there's more or less consensus on a bunch of different inline conditional functions in #5009. I've even been playing around with an (unofficial) draft for an if()
with IACVT behavior here, feedback very welcome.
If computed style of parents is known when matching descendants, I wonder if we can make it more powerful, and use the same general value comparisons instead of something specific to custom properties.
Although do note that a potential problem with this strategy is that the previous compound selectors are not always ancestors, they may be previous siblings, and in the future, as new combinators get introduced, potentially anywhere in the tree. See my comment on #5979 that proposed as similarly subject-excluding pseudo-class.
*
In Chrome that is. Hopefully also in other browsers.
š¤š¼
That is a pretty interesting proposal, indeed. We'd want to be strict about the ordering that you were allowed to query, to prevent loops with, say, :nth-last-child(1 of :const(...))
being able to reference subsequent siblings; it would be restricted solely to caring about the values on elements preceding you in tree order.
We'd relax all restrictions related to the proposed const-variables, since we wouldn't need to introduce them:
:const()
would refer to the existing custom properties. When matching selectors for a given element, the computed values of the parent/ancestor/preceding-sibling is already known*, so it should be fine-ish to depend on those computed values during selector matching of a subsequent element.
The main issue with this is that this would make DOM APIs like .matches()
etc need to update style of all ancestors, etc, which would be a bit unfortunate, imo.
Right, yeah. I always forget about that. The same (approximately) goes for the "const var" approach though. I guess if we want .matches()
to not require a clean style, any kind of conditional pseudo involving custom-props (or computed value time stuff) is out of the question.
What about having the computed variables of a container being one of the things that can be queried by container queries?
#component {
/* Approximately the current state of bikeshed on how to mark a container,
if I understand Issue 6174 correctly. */
query: custom-properties;
}
@container (--pill=on) {
.box { ... }
}
This doesn't really add any significant complexity*, since knowing the computed style (and even layout) of the container before evaluating the container query is already needed for normal (size) queries.
We'd then avoid Emilio's concern with Element.matches.
cc @mirisuzanne
*
I think. Famous last words.
Interesting. Would we be able to style #component
itself based on --pill
or only descendants?
Only descendants unfortunately, otherwise we get circularity problems.
It's not ideal, but combined with an inline if()
(that triggers IACVT) it could work.
Shadow DOM elements would be able to style themselves based on properties on :host
, right?
I don't think container-queries+ShadowDOM have been thought about thoroughly yet, but yes that sounds like what we'd want.
That's the major use case, so whatever solution we pick needs to be able to work with that.
Is the @container
solution able to do other kinds of conditionals too? E.g. 1vw > 1em
? Or only one custom property compared with one value? It would be nice if people can compare values of entire expressions instead of having to basically solve for x to find what the bounds are for the custom property, but that's a nice-to-have, not a dealbreaker. That would also allow them to have conditionals that depend on multiple custom properties.
That should be possible, provided that we can define what the operators mean and how things should evaluate. For example, in 1vw > 1em
both sides might resolve to pixels, but for --x > 1em
the left hand side resolves to a token sequence, the right hand side to pixels, so we'd need rules for how all of that should work. (In this case maybe @property
is needed to make --x > 1em
allowed)?
It would be nice if people can compare values of entire expressions
Mostly a matter of specifying the grammar + rules I think. Totally possible.
Sure, but a lot of these rules are needed for if()
anyway. The extra complexity of the @-rule is that they need to resolve in a way that is property-independent.
I just noticed this discussion after @johannesodland mentioned here in my proposal (#7273) for addressing the same issues in a different way. I completely agree about the needs we have here as @LeaVerou explained.
Do you find useful, a solution like the below by having map-get
function (as in older versions of SASS) to address the issues mentioned here?
<my-button>Save</my-button>
my-button {
--size: small;
}
@media screen and (max-width: 900px) {
my-button {
--size: large;
}
}
/* inside my-button component style */
:host {
padding: map-get((small: 4px 8px, regular: 8px 16px, large: 12px 24px), var(--size, regular));
}
More details are in #7273.
With the proposal in #3714 slightly extended for conditionals, a solution could look something like this:
$pill {
border-radius: 999px;
}
my-input {
@include pill if (var(--pill) = "on");
}
This can be implemented in CSS today using cyclic space toggles (I like to think of them like enums and switch cases).
.foo {
--size: var(--small);
--theme: var(--dark);
--placement: var(--top);
--orientation: var(--vertical);
--significance: var(--warning);
}
Although it isn't the most convenient, here's how something like --size
can be implemented as a library author:
/**
* ## CSS library author code: ################################
*/
button {
/* define the configurable option for the end user, with its default value */
--size: var(--size-medium);
/* define all the possible enum values for the option (commas are required, they enable the magic) */
--size-small: var(--size,);
--size-medium: var(--size,);
--size-large: var(--size,);
/* finally apply output values using a switch case inside of each output property */
border:
solid deeppink
var(--size-small, 1px)
var(--size-medium, 2px)
var(--size-large, 3px);
border-radius:
var(--size-small, 2px)
var(--size-medium, 4px)
var(--size-large, 6px);
padding:
var(--size-small, 2px)
var(--size-medium, 4px)
var(--size-large, 6px);
}
Here's how an end user would use it:
<button id="one">button</button>
<button id="two">button</button>
<button id="three">button</button>
/**
* ## End user code: ##########################################
*/
#one {
/* end user picks the enum value they want */
--size: var(--size-small);
}
#two {
--size: var(--size-medium);
}
#three {
--size: var(--size-large);
}
where, depending on the picked --size
, the component library will set a bunch of properties such as border-width
, padding
, margin
, height
, to various different values depending on the single value of --size
, where --size
is not just a length value.
codepen example: CSS enums and switch-case logic
Roman Komarov has some nice articles on it here: https://kizu.dev/layered-toggles/
TLDR: yeah, maybe not entirely intuitive. But! Once the pattern is learned, it is easy to implement in libraries, and most importantly super easy for the end user to use.
Using one of the syntax ideas above, here's the same thing in would-be new format:
button {
--size: medium; /* possible values: small, medium, large */
border:
solid deeppink
if(var(--size) = small, 1px)
if(var(--size) = medium, 2px)
if(var(--size) = large, 3px);
border-radius:
if(var(--size) = small, 2px)
if(var(--size) = medium, 4px)
if(var(--size) = large, 6px);
padding:
if(var(--size) = small, 2px)
if(var(--size) = medium, 4px)
if(var(--size) = large, 6px);
}
#one {
--size: small;
}
It is not a ton more terse, but the main advantage is that there is no cognitive overhead needed as with the space toggles. Someone who sees this code will know how it works without having to learn how space toggles and property cycles work.
Paired with @property
, IDEs could support type checking:
@property --size {
syntax: "small | medium | large";
inherits: true;
initial-value: medium;
}
(namespaced property names highly recommended)
Iām well aware of cyclic toggles, but they are a hack/workaround, not something we can be content about as a solution and have quite a lot of limitations, not to mention the awkwardness of actual values having to be hidden behind variables.
@kizu who came up with them is actually an Invited Expert in the group and has exactly the same opinion ā in fact we used the ugliness of this and other hacks as an argument to convince the WG to resolve for if()
in #10064
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
attributeorientation
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
vsborder-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
?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 toinitial
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.Current status (updated April 5th, 2021)
This is a long discussion, this is the current status:
if()
has several drawbacks and would end up being very confusing for authors.:const()
selector. This requires two cascade passes. Constants cannot be set based on:const()
selectors.