w3c / csswg-drafts

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

[css-nesting] (Lexical?) scoping for identifiers defined in nested blocks #6809

Open LeaVerou opened 2 years ago

LeaVerou commented 2 years ago

We have currently resolved that the names defined by rules like @keyframes or @property are tree-scoped, and that does resolve the pressing issue of using them in Shadow DOM. However, it is false to assume that any and all encapsulation will ever happen via Shadow DOM. Even in light DOM, authors often use multiple stylesheets, or even write long stylesheets that benefit from actual scoping, in the same way that even JS code written by a single person benefits by not having every variable be global.

Currently Nesting is mainly syntactic sugar, but it can solve these issues quite nicely. Consider the following:

.foo {
    @keyframes fancy-fade { to { filter: opacity(0) } }

    & .baz {
        animation: 1s fancy-fade;
    }
}

.bar {
    @keyframes fancy-fade { to { opacity: 0 } }

    & .yolo {
        animation: 1s fancy-fade;
    }
}

Currently, the syntax above would be transformed to:

.foo {}
@keyframes fancy-fade { to { filter: opacity(0) } }
.foo .baz { animation: 1s fancy-fade; }

.bar {}
@keyframes fancy-fade { to { opacity: 0 } }
.bar .yolo { animation: 1s fancy-fade; }

I'd argue there is literally no case where you want fancy-fade defined inside a block to be available outside said block, and that there's no case where you'd want the animation specified in .foo .baz to use the fancy-fade keyframes defined inside .bar (which is what would currently happen).

What if we apply lexical scoping for name-defining rules specified inside another CSS rule, and enforce it at parse time? It provides a very natural way to scope things, is fully backwards-compatible (since nesting has not been implemented yet), and since it's parse-time, it might be reasonable to implement.

LeaVerou commented 2 years ago

@tabatkins thoughts? Is this feasible?

tabatkins commented 2 years ago

Sorry, I think I have a half-completed response sitting on my other computer. ^_^

From a technical perspective, there's nothing wrong here - behind the covers we'd uniquify the names, and just make sure to keep the original name around for serialization purposes. Not hard.

From a usability perspective tho, I'm not sure about this. The lexical scoping means there's no way to set one of these values from script; it can only be used/referenced by manipulating rules directly in the OM (which sucks to do). If you tried to, it would instead reference the global version of the name (if it existed). We already have this as a minor problem with tree-scoped names in shadow DOM, but it's (a) somewhat rare to set a value in a shadow dom context that you inherited from outside, and (b) usually works anyway, so long as you don't have a name collision. I think this proposal would end up wanting to be able to set things more often, but would never work.

In particular I think this:

I'd argue there is literally no case where you want fancy-fade defined inside a block to be available outside said block

is probably wrong? I'd assume you'd want to use it just as often as you'd set any other animation in script, and similar for any other name-definer.

(Just in general, I think CSS has a very limited capability to scope things in the first place. We don't have the ability to store and pass references around, which makes a lot of this info-hiding impractical in practice, since we can't use ocap mechanisms to expose them where desired. We instead have to work within the limitations of tree-scoping and/or shadows, which is appropriate for some things but not most.)

Loirooriol commented 2 years ago

If they are not scoped, then I think that @keyframes should be invalid when nested. Otherwise seems confusing.

tabatkins commented 2 years ago

Yes, that's currently the case. Only @media and @supports are specced to be allowed inside a style rule.

LeaVerou commented 2 years ago

Ok then, this is a proposal to allow them and scope their identifiers lexically.

romainmenke commented 2 years ago

I am not convinced that the example given with @keyframes should be resolved by introducing a scoping mechanism.

Dev tooling can detect if multiple @keyframes have the same name and throw an error. (e.g. linting) 3rd party styles can use the shadow DOM.

Wanting to reuse the same name for a different thing is maybe not a good pattern?

LeaVerou commented 2 years ago

I don't think it's reasonable to expect that any and all encapsulation happens within Shadow DOM and in the light DOM anything goes and everything should be global. Authors often want scoping as a means to manage their code more easily and create reusable chunks, and asking them to use Shadow DOM every time they want to modularize their CSS code is unrealistic.

Loirooriol commented 2 years ago

How would it work in CSSOM? In the original example, what would this return?

let el = document.querySelector(".foo .baz");
getComputedStyle(el).animationName;

Would this be no-op?

el.style.animationName = getComputedStyle(el).animationName;
LeaVerou commented 2 years ago

@Loirooriol The specified animation name, same as what would be returned if the animation name didn't exist at all. It doesn't get dropped from the CSSOM.

tabatkins commented 2 years ago

I don't think it's reasonable to expect that any and all encapsulation happens within Shadow DOM and in the light DOM anything goes and everything should be global. Authors often want scoping as a means to manage their code more easily and create reusable chunks, and asking them to use Shadow DOM every time they want to modularize their CSS code is unrealistic.

Right, that's why I said:

(Just in general, I think CSS has a very limited capability to scope things in the first place. We don't have the ability to store and pass references around, which makes a lot of this info-hiding impractical in practice, since we can't use ocap mechanisms to expose them where desired. We instead have to work within the limitations of tree-scoping and/or shadows, which is appropriate for some things but not most.)

If shadow-based scoping is too restrictive, then tree scoping, rather than lexical scoping, is the major way I think CSS should pursue this sort of thing. Without the ability to mint and reuse object-capability references, like JS can, lexical scoping ends up being very restrictive and not very compatible with the other interfaces into CSS.

Loirooriol commented 2 years ago

@LeaVerou OK, but then what happens if you reassign the name? Then it suddenly refers to a global one? Or it somehow keeps using the local scope of the rule that would win the cascade without the inline style or something?

LeaVerou commented 2 years ago

I think tree-based scoping could be surprising in some cases, but certainly better than nothing. Would Nesting introducing tree scoping for identifiers and registered custom properties be an option @tabatkins?

tabatkins commented 2 years ago

In theory, sure? I think it would be a significant extra complication impl-wise, tho. Do we have evidence of this starting to become a problem? Are people currently commonly doing manual or tool-based uniquifying of names to work around this?