[css-properties-values-api] Mass property registration
Twitter thread: https://twitter.com/James0x57/status/1467601715067342853
It's rather common to register a number of properties with the same descriptors. E.g. most <integer>, <number> or <length> properties are registered to have an initial value of 0 and most properties are registered to have inherits: true. So, the multiple @property rules can get quite repetitive. For a particularly egregious example of this, look here.
This proposal is about two things:
@property should be able to define multiple properties with the same descriptors by just comma-separating them.
This is such low hanging fruit, that the MDN and web.dev docs on @property originally included it even though it was never a thing on the CSS side!
It should also be pretty easy to do, if it's just a parse time shortcut that is expanded into multiple rules for the CSSOM.
We could even define them to cascade, like so:
@property --number, --scale {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
@property --scale {
initial-value: 1;
}
CSS.registerProperty() does not need to change, since if you're in JS-land, you can just loop anyway.
Authors should be able to register entire namespaces of properties by using a wildcard at the end.
Something like this would help condense the 4000 lines of property registrations here to just one. Admittedly, this is not a realistic example of regular CSS, but I've definitely come across more real cases of duplication that could be eliminated this way.
Another use case is utility registered properties like --int-*, --length-* etc see tweet by @propjockey.
@tabatkins said they think it is feasible.
These would also need to be combinable, to deal with potential conflicts:
@property --int-* {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
@property --int-foo-* {
initial-value: 1;
}
@property --int-foo-bar {
initial-value: 2;
}
Since <dashed-ident> does not permit *, I suppose we'd need to define a new token, <dashed-ident-group>
CSS.registerProperty() could just accept these tokens under name, to minimize API changes, or we could do something more fancy on the JS side.
CSSOM wise this cannot really be expanded at parse time, since the set is infinite. Perhaps there should be some read-only boolean for authors to check if this is a group without having to do rule.name.endsWith("*"), but that's a minor point.
The first one (multiple comma-separated names in the registration) we should definitely do, no question. Such an obvious improvement.
The second (wildcard registration) is something I support if it's feasible on implementations. No need to get fancy with the API, it'll just be a new name form; an <ident-token> followed by a "*" <delim-token>.
Your idea of combining them if multiple apply is interesting, but new to the API; currently each definition is independent, and will override each other if they collide names. I'd assumed we'd use longest-prefix-match semantics, so in your example --int-foo-1 would use the @property --int-foo-* block, but not the @property --int-* one.
Oh, but:
It should also be pretty easy to do, if it's just a parse time shortcut that is expanded into multiple rules for the CSSOM.
It shouldn't be expanded into multiple rules. We can either make the .name property an array now, hoping the compat is fine, or keep it a string that might be comma-delimited.
make the .name property an array now, hoping the compat is fine, or keep it a string that might be comma-delimited.
Then it should probably also be renamed to names to indicate that it can hold multiple property names, if there are no compat issues.
Sebastian
Got sent a similar request via Twitter: https://twitter.com/LukyVJ/status/1627624292484542467
With @property shipping in browsers other than Chrome soon, and more people using it, I predict more similar requests in the near future.
For at least 4 years now, everybody has agreed the syntax for @property should be :
@property <custom-property-name># {
<declaration-list>
}
but in the current draft it still lacks that #.
For at least 4 years now, everybody has agreed the syntax for [
@property](](drafts.csswg.org/css-variables-2#typedef-custom-property-name) should be :@property
# { } but in the current draft it still lacks that #.
If you can point to a resolution for the "everybody has agreed" I will commit the change!
There is another proposal here: https://github.com/w3c/csswg-drafts/issues/7523#issuecomment-1683831209 (based on Tab's post immediately before):
@property {
syntax: "<length>";
inherits: false;
initial-value: 2147483647px;
--width: initial; /* 2147483647px */
--height: initial; /* 2147483647px */
--depth: 0px;
}
And/or we could imagine that a hypothetical initial() function is allowed here, to be able to express an initial value in terms of other initial values:
@property {
syntax: "<length>";
inherits: false;
--one: 1px;
--two: 2px;
--width: initial(--one);
--height: initial(--two);
--foo: calc(initial(--one) + initial(--two));
}
That would require a novel concept of "computing" an initial value, but that should be possible as long as everything is computationally independent otherwise.
Mass registration with wildcards: so ... @property --int-* is basically something like a "selector" that "matches" custom property names on that form? I'm not sure if it's wise to bake that much (potentially expensive) complexity into the the computed value process. Hopefully we can consider other ways to solve the use-cases this would be needed for. For example, custom functions/if() could help for some of them?
Mass registration with wildcards: so ...
@property --int-*is basically something like a "selector" that "matches" custom property names on that form? I'm not sure if it's wise to bake that much (potentially expensive) complexity into the the computed value process. Hopefully we can consider other ways to solve the use-cases this would be needed for. For example, custom functions/if()could help for some of them?
I do think we should introduce features that eliminate the need for the hundreds of custom properties currently needed by design systems, and yes custom functions and if() will help tremendously, but I don't think we'd ever get it down to such small numbers where mass-registration won't be desirable. And there is a nice decoupling in being able to write
@property --foo-color-* {
syntax: "<color>";
initial-value: transparent;
inherits: true;
}
rather than having to list every color design token in a centralized place.
https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
append to decls all the custom properties whose computed value for obj is not the guaranteed-invalid value.
How do you conciliate this with mass registration? With --foo-color-*, there would be an infinite amount of such properties.
We'd have to say that @property --foo-color-* doesn't actually count as those (infinitely many) properties existing, but instead just affects the computed value process for the actually-specified custom properties that match.
I don't get it, what makes one of these infinitely many properties exist for real? Being used in var()?
The same thing that makes an unregistered property exist for real: specifying it somewhere. E.g. for #foo { --thing: 1; }, --thing shows up in the gCS enumeration on <div id=foo>, but the infinite amount of unregistered custom properties that could have been specified do not.
The same thing that makes an unregistered property exist for real
It's not the same thing, because for unregistered, the logic is that the value is not the guaranteed-invalid value. E.g. this is false:
<!DOCTYPE html>
<style>:root { --foo-bar: initial }</style>
<script>
// This is false, the value is the guaranteed-invalid value
document.documentElement.textContent =
[...getComputedStyle(document.documentElement)].includes("--foo-bar");
</script>
For mass registered properties whose initial value is not the guaranteed-invalid value, the above doesn't work. So instead of checking the computed style, we need to check the cascade. And then this would be true?
<!DOCTYPE html>
<style>
@property --foo-* { syntax: "<number>"; initial-value: 1; inherits: true }
:root { --foo-bar: initial }
</style>
<script>
// This is true, the property is specified
document.documentElement.textContent =
[...getComputedStyle(document.documentElement)].includes("--foo-bar");
</script>
And what about inheritance? Does the property count as specified if it inherits from an ancestor where it's actually specified?
And what about revert & friends, do they count as specifying, or can they undo the specification of the property?