w3c / csswg-drafts

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

[css-properties-values-api] A compact syntax for registering global constants/custom properties #9206

Open mirisuzanne opened 1 year ago

mirisuzanne commented 1 year ago

There has been discussion in several issues recently about providing a ::document selector, or @document/@global/@env rule that would allow authors a quick way to register globally available constants (as custom properties or environment variables) - without a full @property rule describing each individually. See, for example:

(Hopefully this issue is useful as a way of combining several discussions, and not just a duplicate)

Those first two issues have more specific concerns that could be (or have been) addressed in specific ways - but mention the possibility we might still want a more generic solution for registering global properties. Custom media queries (@custom-media) have similarly been solved as a specific case, but not yet implemented – and there's an open issue for adding @custom-container. Those use-cases could also be solved by a well-defined global parameter registry.

There are two primary overlapping concerns here:

I tend to agree with @tabatkins that:

Having an author-defined env() might work, but would require an author to duplicate values across properties and env(), or else decide for each whether they want to express it as a var or an env().

I like the proposed solution of allowing global reference to the initial values of registered custom properties, but think we need to make that registration simpler for it to work. I don't know the right syntax for that, but if we're able to come up with something compact, it could also be useful for defining parameters in declarative custom functions and (someday, maybe) mixins.

The @property rule can register a custom property name with three associated descriptors:

Authors often skip this registration for most properties, and only define the name along with an un-registered not-technically-'initial' value on the root element. Then some authors add the @property registration for specific properties - often to define a syntax for the sake of interpolation, but occasionally to set inheritance or a more formal initial value.

Things I would expect from a syntax:

The main complexity that I see with defining a compact syntax is that custom property values (including initial values) are very permissive. It will be difficult to combine initial-value with anything else on the right side of a standard property:value; format – though it should be possible using either the ! delim-token, or wrapping ()/{}/[] of some kind. The other thought I had was to put some of the definition on the left side. Some rough ideas:

@global {
  --my-color("<color>"): mediumvioletred;
  --my-padding("<length>"): 10px !no-inherit;
}

@global "<color>" no-inherit {
  --brand-color: teal;
}

The an all-in-one syntax is more appropriate for reuse in function parameter definitions, while the grouped syntax helps with defining a whole set of related variables in a single place.

On the other end, when calling custom properties, a env() or global() function could give access to the initial registered value anywhere in the document, while var() returns only the cascaded value. I prefer this to a context-dependent resolution of var(), since it provides more clarity on what value to expect, and makes both available where var() is already supported:

.my-thing {
  color: var(--brand-color);
  padding: global(--my-padding);

  @media global(--above-small) {
    ...
  }
}

This is far from a complete proposal, and mostly a request that we consider these use-cases together. Happy for thoughts, and willing to merge this into the env() conversation if it belongs there.

jimmyfrasche commented 1 year ago

Possibly related #7523

andruud commented 1 year ago

Yeah, I'd rather solve custom at-rules this way than adding @custom-whatever for everything with a prelude.

@global "<color>" no-inherit { --brand-color: teal; }

If this does exactly the same as @property, then we should try to make it a special form of @property intead:

@property "<color>" no-inherit { --brand-color: teal; }

That should be doable as long as the new prelude doesn't start with --.

mirisuzanne commented 1 year ago

Currently, the rules for property registration severely limit the initial value of any property with non-universal syntax – requiring them to be 'computationally independent'. For example, a property with <length> syntax cannot have an initial value of 3em. Even without a syntax, variable cross-reference is not allowed

I don't know the original purpose of that restriction, but it would be a potential blocker here. The goal would be for authors to use this in place of setting all custom properties on root. But many custom property use-cases rely on relative units and relationships between variables. If those things aren't allowed in the registration syntax, then authors will still be forced to define custom properties in two places.

tabatkins commented 1 year ago

This issue is doing two separate things: mass registrations and making things "global". We should keep those topics separate; mass registration is already in #7523. (And I've just dropped a syntax suggestion over there that I quite like, modulo the rule name.)

For "global" things, there are two possible behaviors you could be invoking:

  1. The property should be available at the "global" level, and usable in other at-rules, MQs, etc with its initial value.
  2. The property should be static, and not changeable at different parts of the tree. (This might help with the perf issues from having several hundred or even thousands of custom properties for a design system.)

Calling back to my env() comment, I think we can cleanly achieve 1 without even having to do anything by just letting env() take a dashed-ident. (Well, technically it already does allow that, but no dashed-ident keywords are defined for it or ever will be.) If the "environment variable name" is dashed, it just looks up the initial value of the corresponding custom property, done.

For 2, this could just be a new descriptor in the registration block. Dunno what a good name is, tho.

tabatkins commented 1 year ago

Yeah, I'd rather solve custom at-rules this way than adding @custom-whatever for everything with a prelude.

I'm not sure what you meant by this, @andruud.

mirisuzanne commented 1 year ago

For "global" things, there are two possible behaviors you could be invoking

I was in fact invoking behavior 1, and mentioned env() as an option. Glad to hear that could work reasonably well.

I'm open to closing this as a duplicate of:

But the reason I opened this issue in the first place is to capture where they overlap in terms of author use-case and needs. Because they do. Currently @property is not a reasonable solution for defining either local or global variables, and authors would like a solution that can handle both in one place. So our solutions have to work together in an elegant way here. Because, from an authoring perspective, they are part of the same project of defining the initial/global state of variables.

For example, the limitation I mentioned above is a likely issue for both proposals. Initial values restricted in that way will not cover the use-case for global values, and will also not act as a replacement for defining custom properties on the root element.

(I linked @custom-media above, and mentioned global variables could be used to achieve the same thing. I believe Anders was referring to that.)

andruud commented 1 year ago

For example, a property with syntax cannot have an initial value of 3em.

I think we probably already rely on initial values being in absolute/cononical-unit form, but assuming it's possible, it could be very bad for performance. In the worst case, it will be as if you specify all your "global" custom properties with * { ... } instead of :root { ... }. But I definitely see your point.

Even without a syntax, variable cross-reference is not allowed

But maybe we can allow env()?

If the "environment variable name" is dashed, it just looks up the initial value of the corresponding custom property, done.

That actually sounds very nice? And using env() makes it clear that it's a not an element-specific lookup.

I'm not sure what you meant by this, @andruud.

(I linked @custom-media above, and mentioned global variables could be used to achieve the same thing. I believe Anders was referring to that.)

Yeah, I meant that if you can substitute arbitrary stuff into @media (...) with env(), then probably @custom-media isn't needed. Nor is @custom-container, nor @custom-future-thing.

romainmenke commented 1 year ago

Yeah, I meant that if you can substitute arbitrary stuff into @media (...) with env(), then probably @custom-media isn't needed. Nor is @custom-container, nor @custom-future-thing

The current @custom-media is defined in such a way that they are boolean features when used. It is not a token or component value substitution.

@custom-media --foo only screen and (min-width: 300px);

@media (prefers-color-scheme: dark) and (--foo) {}

Can the same be achieved with env()?

CSS authors really like being able to write @media (--tablet) {} with tooling.
@media (min-width: env(--tablet-width)) won't be as handy.

argyleink commented 1 year ago

There's a few variable management issues getting attacked here! Love to see it 😍


from https://github.com/w3c/csswg-drafts/issues/9206#issuecomment-1683089672

If the "environment variable name" is dashed, it just looks up the initial value of the corresponding custom property, done.

env(--screen-sm), usable in places where var() is not, like @custom-media, @media, @container, etc? lgtm 👍🏻


from https://github.com/w3c/csswg-drafts/issues/9206#issuecomment-1683089672

The property should be static ... this could just be a new descriptor in the registration block. Dunno what a good name is, tho.

what about immutable? syntax based on https://github.com/w3c/csswg-drafts/issues/7523#issuecomment-1683831209

@property {
  syntax: "<length>";
  inherits: false;
  immutable: true;
  initial-value: 2147483647px;

  --width: initial; /* 2147483647px */
  --height: initial; /* 2147483647px */
  --depth: 0px;
}


That covers:

  1. props like consts, usable places var() is not. this is basically env() adding -- support
  2. bulk typed custom property registration, getting tackled in https://github.com/w3c/csswg-drafts/issues/7523

Missed?

  1. a global selector to register props in that fixes the misconceptions about :root
  2. authors would like a solution that can handle both in one place from https://github.com/w3c/csswg-drafts/issues/9206#issuecomment-1683125310

Seems like we still need ::document, @document, @global global space. Local vars being global isnt solved with the @property bulk syntax or env() vars.

Loirooriol commented 1 year ago

immutable: false is a double negative that should probably be mutable: true instead.

But I think I prefer global, to make it clearer that these will be globally available.

It can also make sense adding the other option described by Tab: static props accross the tree. I can imagine wanting to change some color props depending on a media query checking prefers-color-scheme, so the props can't be used in media query conditions, but wanting to avoid perf problems if there are lots of these props.

LeaVerou commented 1 year ago

I agree with @tabatkins that mass registration and global variables are separate issues.

It would be good to support a way of declaring global declarations that doesn't just center around custom properties, to address the needs for things like ::document and friends that keep popping up (here's one from just the other day)

An important requirement for these globals is that they need to be able to express values that depend on other globals, otherwise their utility for things like design systems will be limited.

For example, consider:

--color-magenta-h: 335;
--color-magenta-s: 90%;
--color-magenta-l: 50%;
--color-magenta: hsl(var(--color-magenta-h) var(--color-magenta-s) var(--color-magenta-l));
--color-accent: var(--color-magenta);
--color-accent-lighter: hsl(from var(--color-accent) h s calc(l * 1.2));
/* etc etc */

(sure, wider adoption of CSS Color 5 will reduce the need to specify components separately, but the rest still applies...)

Would the env() solution support that? I suppose it could be defined as basically "var() that only draws from properties defined within a ::global rule" (or whatever we call it).

mirisuzanne commented 1 year ago

Yes, that's why I think the issues are related. From an authoring perspective, the way design tokens are often managed, I want that same ability to create related tokens in both local var() and global env() values – and often have them match. I'm proposing exactly the logic above: "var() that only draws from properties defined within a ::global rule".

Design tokens in practice do not fall into neat 'global-and-independent' vs 'local-and-relational' categories. If we are providing both a way to make tokens globally available, and a way to define tokens in bulk, those two features are fundamentally part of a the same author use-cases. We can certainly implement them separately, but they have to play well together as part of a shared design-token-management work flow.

LeaVerou commented 1 year ago

@mirisuzanne so to make sure I understand: env(<custom-ident>) would resolve based on the regular custom properties defined ONLY in ::global rules? And these regular custom properties would be usable via var() as well, with ::global sitting above :root in the inheritance chain?

mirisuzanne commented 1 year ago

@LeaVerou Yes, that's what I had in mind.

LeaVerou commented 1 year ago

I think that would be great, if it's implementable.

mirisuzanne commented 1 year ago

@mirisuzanne so to make sure I understand: env(<custom-ident>) would resolve based on the regular custom properties defined ONLY in ::global rules? And these regular custom properties would be usable via var() as well, with ::global sitting above :root in the inheritance chain?

To clarify this a bit more: I would have env(<custom-ident>) resolve based on the regular custom properties as defined in the global rule only. Env would always return the global value of the property. But I would not have a restriction that those custom properties remain constant throughout the cascade. Custom properties could get different contextual values accessible to var(), and that would have no impact on their global value or access via env().

tabatkins commented 1 year ago

The handful of issues have gotten entirely mixed up and cross-talking, so I'll just link to my comment in #7523 about why it's still important to have immutability as a (perf-oriented) feature.

kizu commented 1 year ago

As an author, I would really-really want almost everything proposed in this issue and the comments below.

In 2020 I did propose to make some of the fields in @property optional (https://github.com/w3c/css-houdini-drafts/issues/994), but my attempt was not successful at that time.

Maybe this issue could lead to us having a better way to define both custom properties, and global immutable tokens in a much more convenient way.

One thing I want to add/ask: does someone know of any benchmarks/research on how the inheritance of the custom properties impacts performance?

What could be potential gains from a situation where we, let's say, convert a thousand of design-tokens from a :root defined just as --foo, this being inheritable, to immutable global env variables, from a browser engine standpoint? Probably could be tested right now with the @property, but we don't have any good tools in the current browser developer tools to measure this outside of the “black box” that is currently all the style recalculation/layout boxes in the performance flamegraphs.

Que-tin commented 11 months ago

Quick question. What about non global constants?