Closed sorvell closed 1 year ago
I like this idea a lot. One thing about it that it solves is the issue of wanting to supply a "UA stylesheet" for your custom elements. You can't just do something like my-element { display: block; }
because this fails to reach inside shadow roots. When we had deep, you could do something like my-element, * /deep/ my-element { display: block; }
, but deep is gone. Also, that formulation is higher priority than the UA stylesheet, as noted in HTML as Custom Elements. (In particular, something like * { display: inline; }
should override this "UA stylesheet level" stuff.)
One thing I am still confused on is whether :host
is the right name for the selector targeting the specific element being defined. Since we're specifically not talking about inserting a shadow root, and :host
is currently defined in terms of shadow DOM, it seems like a bad match to me, and I'd expect something new, more like :element
. But others have told me :host
is a good idea. If we sufficiently redefined :host
as such (@TabAtkins, @hayatoito?) then maybe it's OK. I'd still like to understand why we'd do that though.
+1 from me on this idea; I'd wondered about an author-controlled "UA styles" a few years ago, early in the project, and this API surface feels good, better than whatever else I'd come up with.
I presume that the selectors are meant to be interpreted "like" a shadow stylesheet, right? So they only apply to the element and its contents, and don't leak out to the global level? In that case this is pretty easy - just need to specify that the styles are interpreted in the user-agent origin and all of its selectors are scope-contained by the host element.
I think using :host
is fine - it has a very similar meaning here to what it does in shadow DOM, and that makes things easier to understand. Do we plan to "hide" the host element, like you get in real shadow DOM? (I think we should, to keep the similarities as high as possible.) If so, then we just need to additionally say that, for the purposes of these selectors, the root element is featureless and matches :host
/:host()
/:host-context()
. (This is more-specific-overriding-general, contradicting the general rule that :host
/etc represents nothing outside a shadow tree.)
Question: do we want to necessarily tie this to the registerElement() call? Or do we want to allow embedding this into your global CSS too, so that even if your JS is slow or broken, your page at least doesn't totally break? We'd put it under a @custom-element foo-bar {...}
rule or something similar. We'd just need to define an ordering for the JS sheet vs the CSS ones; I suspect the JS sheet should come afterwards, so they'll win in case of conflict.
See also https://github.com/w3c/webcomponents/issues/376 (closed), where I had a similar idea.
I really like this idea too, though it might be worth having a custom element-specific stylesheet instead:
let sheet = document.createElement('style');
sheet.textContent = ':host { display: block; }';
customElements.define("cool-element", CoolElement, { styleSheet: sheet });
This way you can retain a reference and add to, remove or modify the css rules in a familiar manner later if required.
Yeah, having the IDL be (DOMString or CSSStyleSheet)
would be good. Don't want to require the object-creation dance if it's something simple.
See also #376 (closed), where I had a similar idea.
Interesting! Do you think this more limited approach (just giving a stylesheet to a custom element) is sufficient, or do you still think we need the ability to target arbitrary elements with a compound selector? I can see arguments either way.
(I think we need this current idea either way; providing default styles for a component is good and useful all by itself.)
Interesting! Do you think this more limited approach (just giving a stylesheet to a custom element) is sufficient, or do you still think we need the ability to target arbitrary elements with a compound selector? I can see arguments either way.
It looks that each solves the different use cases. If a selector used in "a stylesheet to a custom element" always has a ":host" pseudo class, both approaches might be able to address the original concern.
If we can assume that ":host" is always used in a selector here, can we have a more lightweight approach?
e.g.
customElements.define("cool-element", CoolElement,
{style: "* {display: block; } *[red=true] {color: red; }",
shadowtreestyle: ... /* if we still need this */ } );
Thus, I would like to limit more and more so that it can become style-engine friendly, ignoring a shadow tree in most cases.
That sounds reasonable to me! I think I agree that if you want to style the custom element's contents more fully, you should probably be using a shadow tree. The behavior of styles in shadow trees already works pretty well.
So this brings your idea from #376 fully in line with the idea from this thread; the {style: "..."}
option just provides a convenient inline mechanism for defining such styles on the element, without having to repeat the tagname over and over if you're defining multiple selectors. We can then lean on your @global-compound-selector-rule
(with a better name, of course ^_^) for the declarative side of things, or if people want to provide the styles in their CSS file rather than in their JS file. (And we can do it in the future; no need to block this thread's idea on figuring out the details of the CSS rule.)
Yeah, I do not have an intention to block this thread's idea. I'm totally fine to let customElement.define
take a style option.
From the conference: There's no real objection to this proposal, but it needs to be more worked out with respect to the CSS cascade and such before it can be properly reviewed. Another concern that was raised is that it would help if there was some kind of holistic overview to styling custom elements and shadow DOM since there appear to be several overlapping approaches.
holistic overview to styling custom elements and shadow DOM since there appear to be several overlapping approaches.
That makes sense. Here the scenarios we've run into so far that need styling:
display
so the page doesn't render totally screwed up. Preferably this should be possible without requiring a shadow root. This must apply thru shadows; that is, it can't be done by a global stylesheet just applying a x-foo {...}
style.Which idea of Hayato are you referring for (4)?
The one that he and I were talking about immediately prior to this, from issue #376.
@sorvell, does @hayatoito's simpler approach in https://github.com/w3c/webcomponents/issues/468#issuecomment-203807233 fit your use cases, or do you also need to be able to style descendant elements?
@tabatkins, would you have some time next week to work on defining this (either @hayatoito's proposal or something closer to the OP) as a more fully-fleshed-out proposal, with maybe some proto spec text? I think we'd need your help (or someone else great at writing CSS specs) to actually figure out how this means, and maybe put the relevant stuff in CSS scoping. How I envision this is HTML just defining the dictionary member and saying something like "this creates a custom user-agent stylesheet for the current Window with element name name," where you can define "creates a custom user-agent stylesheet" for us.
Yeah, def.
@domenic Yes, I think #468 (comment) is a reasonable simplification.
To be clear, I do not think it's a good idea to expose the ability to style descendants (children). The initial proposal did include styling shadowRoot elements (shadowRoot children), but this can always be addressed in the traditional way. Since you're already making a shadowRoot in that case putting a style element there is straightforward.
@hayatoito's approach solves the fundamental problem here: an element has no desire to create a shadowRoot and just wants to style itself cheaply and easily.
Wow, didn't expect this issue to be open just 2 days after #376 was closed. This proposal actually covers all that I expected in #376, so thanks @sorvell for raising it once again.
@domenic
(In particular, something like * { display: inline; } should override this "UA stylesheet level" stuff.)
What about some options to disable all default UA styles?
el.createShadowRoot({defaultStyles: false})
From my lesser understanding of all Web Components compared to you all, it seems as though ShadowDOM roots are designed to be the units of encapsulation for DOM. If that is the case, it seems like encapsulating styles in this programmatic manner might be a better fit for the el.createShadowRoot
method. For example:
el.createShadowRoot({styles: ':host {display: block; }')
This particular example seems to make more sense because :host
refers to the host of a shadow root where the style is located, making createShadowRoot()
seem like the obvious choice over createElement()
or define()
. There's nothing that prevents a Custom Element from having multiple shadow roots, so :host
in the style of a custom element is ambiguous (as implied by @tabatkins above). @tabatkins also brought up that there would need to be a new mechanism to scope the style to the custom element (because there's no extra benefit to the API addition if it just creates global style), but such a style scoping mechanism already exists on ShadowDOM roots.
TLDR, would it make sense to move this programmatic definition of a style onto shadow roots where style encapsulation already exists, instead of on Custom Elements where we have to define new mechanisms to deal with possible ambiguities and style scoping issues?
There is at least one case when it is not possible - when you extending native elements. They have UA's ShadowRoot, but you neither have access to it, nor have ability to create new one on them, look at #376 for more discussion. Also this particular issue is exactly about avoiding performance overhead of creating Shadow Root (see the first message in this thread).
@nazar-pc Thanks for pointing that out. Maybe we can have both?
customElements.define("cool-element", CoolElement, {styles: '.thing { color: blue; }');
el.createShadowRoot({styles: ':host {display: block; }') // using :host makes sense here
@domenic All right, first draft is up. I forgot to give the heading an ID, so just visit https://drafts.csswg.org/css-scoping/#shadow-dom and scroll up.
Let me know if this suits your needs and if you need anything changed. Idea is that DOM would parse the string to a stylesheet and then manipulate the [[defaultElementStylesMap]]
itself.
@tabatkins that looks great! Where's the algorithm for parsing a string into a spec-stylesheet that I can use as the value in a map entry?
We're having a hard time following this discussion because there is a lot of different use cases and ideas being discussed here. Could someone compile a list of concrete use cases that are meant to be addressed by this issue?
I think there are two main cases:
Styling elements that do not otherwise need a shadow root
We've encountered a number of elements that create a shadow root only to apply host styling. Their shadow only consists of a <style>
and <content>
. The cost of the shadow root definitely shows up when performance tuning large apps. With this we can eliminate the shadow.
Sharing styling across elements
Currently this requires creating a <style>
element in each shadow, which has costs. Allowing styles to be passed in as a string at definition time is a path to allowing constructible stylesheets to be passed in later.
Additionally, it may address parts of #426 by allowing elements to set display:
at definition time, without needing to create a shadow root. This is really just the first use case, and it probably doesn't address the default value without some helper adding in display: block
value by default.
@sorvell any more from the Polymer customers?
To state the "Styling elements that do not otherwise need a shadow root" use case in a way that is more custom elements-centric:
It's common for custom elements to want to come with default styles---very similar to the "user agent stylesheet" for built-in elements.
You might think that the way to do this is to ship my-element.css
in addition to my-element.js
, with contents like my-element { display: block; }
. (Or something much more complicated, for my-button.)
However, this does not work for two reasons:
<button>
inside a shadow tree, it is styled, but with the above solution, a <my-element>
inside a shadow tree stays display: inline
, since the my-element
selector cannot reach inside shadow trees.The solution proposed here lets custom elements come prepackaged, at definition time without any external dependencies, with default "user agent" level stylesheets. It also allows modifications such as styling conditional on the presence or absence of an attribute, e.g.
customElements.define("my-button", MyButton, { styles: `
* { display: inline-block; }
*[disabled] { color: gray; }
`);
Allowing styles to be passed in as a string at definition time is a path to allowing constructible stylesheets to be passed in later.
This doesn't actually require constructible stylesheets; engines can memoize on the string values. Last time this was discussed constructible stylesheets were more useful just to make the sharing explicit to the developer, and don't provide much, if any, advantage for the engine.
Does this help, @rniwa?
Thanks.
- Styling elements that do not otherwise need a shadow root
Could you give us a concrete list of those elements?
- Sharing styling across elements
We (WebKit) already have a plan to implement an optimization similar to the one @domenic pointed out without needing this new API.
- Most importantly, because it does not work inside shadow trees. When you use a
The solution here is to attach a shadow root on a custom element where such rules need to be applied. Note that we've repeatedly and consistently opposed to having type extensions supported in custom elements so we're less sympathetic to the argument that this is needed for builtin elements that don't support shadow roots.
- Less importantly, but still interestingly, is that the precedence of such stylesheets is higher than the usual precedence of "user agent stylesheets" in the cascade. This has various subtle undesirable side effects.
Could you list a concrete use case and what those undesirable side effects are?
- Styling elements that do not otherwise need a shadow root Could you give us a concrete list of those elements?
This is an unbounded set of author-created elements, so no, a "concrete list of those elements" can't be provided. Justin can probably give a few examples from their own code, tho.
The solution here is to attach a shadow root on a custom element where such rules need to be applied.
As Justin said, in cases where you don't actually need a shadow tree (your custom element is a container with special powers, that doesn't need to provide any special shadow structure), requiring a shadow root + style element is a cost without a benefit.
Could you list a concrete use case and what those undesirable side effects are?
We're just talking about standard specificity battles. You don't have to worry about fighting with another selector when overriding UA styles; they lay the default groundwork, and you can override freely; even if they respond to state in some way, which would require higher-specificity selectors, you can still override without worry.
- Styling elements that do not otherwise need a shadow root Could you give us a concrete list of those elements?
This is an unbounded set of author-created elements, so no, a "concrete list of those elements" can't be provided. Justin can probably give a few examples from their own code, tho.
I think you misread my comment. I said a "concrete list", not a "complete list".
As Justin said, in cases where you don't actually need a shadow tree (your custom element is a container with special powers, that doesn't need to provide any special shadow structure), requiring a shadow root + style element is a cost without a benefit.
We don't think having an extra shadow root and a style element is a cost compared to the cognitive cost of having to remember yet another way to specify style rules. Now in addition to UA & user stylesheets, style rules in document, shadow trees, and inline style declarations, authors need to be aware of style rules that come from custom element definitions. All these complexities makes it harder to authors to reason about already convoluted style resolution. We don't want to make web components so complex that only people working on browsers and libraries can understand.
Could you list a concrete use case and what those undesirable side effects are?
We're just talking about standard specificity battles. You don't have to worry about fighting with another selector when overriding UA styles; they lay the default groundwork, and you can override freely; even if they respond to state in some way, which would require higher-specificity selectors, you can still override without worry.
Why does having a style element inside a shadow tree not solve this problem? Rules in such a style element has a different cascading order than the one author apply outside the shadow tree.
I think from a component standpoint, having something that gives authors access to user-agent-level rule evaluation is huge. It doesn't really make sense for the masses, which is https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Default-Stylesheets-Concept-and-Proposal.md probably isn't a good idea, but limiting to the custom element scenario seems totally legit (since UA's have default stylesheets for elements, why isn't this exposed to authors of new custom elements?)
Why does having a style element inside a shadow tree not solve this problem?
Honestly, from the perspective of an outsider who doesn't develop browsers -- a mere "web developer" -- yet library author working on http://infamous.io, putting styles inside a shadow tree in order to style the shadow host (thus breaking out of the shadow encapsulation back into the light tree) seems exactly like a hack.
Specifying styles directly onto the custom element definition is much cleaner because the style is associated directly with the element being styled (not with it's shadow root), and I appreciate the cognitive load required to work with that much more (@rniwa) in terms of code clarity, separation of concerns, and maintainability. This is what led me to ask #490.
Suppose we have
customElements.define("custom-element", CustomElement, {styles: '.thing { height: 50%; }');
Will the height of div.thing
be 50% of the element where it is finally located (i.e. the element in the flat tree where div.thing finally lives)? Or 50% of the height of the <custom-element>
?
div.thing
cannot be styled by the styles
option. Only custom-element
elements can be styled by the styles
option.
Your example would ensure that custom-element
elements with class thing
would have the style height: 50%
on them. This would then behave according to normal CSS rules for how height
behaves.
@domenic So the *
selector in
customElements.define("my-button", MyButton, { styles: `
* { display: inline-block; }
*[disabled] { color: gray; }
`);
only selects the custom element that the style is defined on?
Correct.
What is the status of this issue? I guess there is no progress.
@sorvell
If we support this feature, can we remove the most usages of :host
pseudo class? I think that would be a win.
The CSS side was complete back in April, defined in https://drafts.csswg.org/css-scoping/#default-element-styles. Only thing that needs to be done is the additional argument to registerElement()
.
customElements.define("my-button", MyButton, { styles: ` * { display: inline-block; } *[disabled] { color: gray; } `);
That seems funky for some reason.
But, while we're at it, we may as well decide to allow passing CSSStyleSheet(text, options), something like
const sheet = new CSSStyleSheet(text, options)
customElements.define("my-button", MyButton, {
styles: sheet
);
I'm not a huge fan of defining the styles as a string either. But presumably UA styles are going to be pretty small so I can begrudgingly live with it.
I like the idea of it accepting a CSSStyleSheet but I would want that stylesheet to be defined in html via a <style>
tag and doing that would have unwanted side-effects. So if the only way to use a CSSStyleSheet is by creating it in JavaScript and appending text to it, I don't see it as an improvement.
FWIW, I like every proposal as long as it works but I am having hard time to understand the plural as object property ... styles
... why is that?
<style>
, not <styles>
{style: sheet}
is more semantic than {styleSheet: sheet}
(which is even worse than styles, IMO)style
or css
to style their components.style
, never styles
, here just an exampleAs silly as this matter could look like, I think having style
as property name doesn't need explanations, while having a plural form would mislead at least once most developers (those I know).
You don't "styles" a component, you "style" a component.
As non native, non mother-tongue, English developer, I think the correct naming would be a plus.
Thanks in advance for eventually considering this point of view.
FYI - I posted a thread about constructable stylesheets at WICG discourse https://discourse.wicg.io/t/proposal-constructable-stylesheet-objects/2572
@jcgregorio's blog post has a (somewhat synthetic but nevertheless impressive in the effect) demo case at the bottom of the prose that can serve as a motivation for having this proposed mechanism.
For customElements
in particular, why not allow for the defining of a custom stylesheet map, such that
customElements.defineStyles('some-map-key', CSSStyleSheet | string | href?);
would set up a map of styles that would only apply to a custom element that somehow references that map key? I can see that going one of two ways:
class MyElementConstructor extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
styles: ['some-map-key', 'some-other-key']
});
}
/* omitted */
}
or
customElements.define('my-element', MyElementConstructor, {
styles: ['some-map-key', 'some-other-key']
});
Thus that any styles attached to 'some-map-key' or 'some-other-key' would be added only to this element, but not the surrounding window. I can see how someone might argue this would break encapsulation, but would allow browsers to optimize rendering without bleeding into the global CSS scope.
It seems this work is blocked on spec'ing constructible stylesheet? FWIW, only requirement we have is that this feature to NOT add a new cascading order.
Why would this be blocked per the constructible stylesheet? I think it should definitely plan to incorporate such a feature, but I don't see any reason why we couldn't load in an external stylesheet to be applied or pass in a string for styles. It might not be perfect, but it should work (although I'm fairly ignorant about browser internals, so I'll accept any feedback on that front).
Also, I do think that following Angular's pattern of applying styles as an array is preferable. I can foresee some cases where a user might want to set up a single stylesheet to set up CSS variables or @apply
rules that might be used across multiple components. It would be a big benefit to be able to define that once and reuse across multiple custom elements. Using a syntax similar to what I defined above you could do:
class MyLibraryComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
styles: [ 'my-library-variables', 'my-library-component' ]
});
}
}
OR
customElements.define('library-component', MyLibraryComponent, {
styles: [ 'my-library-variables', 'my-library-component']
});
customElements.defineStyles('some-map-key', CSSStyleSheet | string | href?);
This is an interesting idea!
I'd go with placing styles on the element, NOT the shadow root. I have many custom elements elements that don't have a shadow root, so adding one just for this functionality would be too heavy.
Why would this be blocked per the constructible stylesheet?
Yep, doesn't need to be blocked, it can just accept a CSS string for now. And href is not needed, @import
can be used in the string.
FWIW, only requirement we have is that this feature to NOT add a new cascading order.
What do you mean? Or do you mean these styles should have highest priority, second only to UA styles?
It seems to me like the order they are defined in the array would dictate Cascade order, and that this would helpful.
It seems to me like the order they are defined in the array would dictate Cascade order, and that this would helpful.
I would also assume that document-level styles that would typically bleed into the shadow DOM would still override (so CSS variables, things like color
, font-family
, etc.).
@rniwa, do you thing having CSSStyleSheet
should be a blocker for this or would Apple be cool with allowing either a CSSStyleSheet
or a text string (dropping href per @trusktr's comment above)?
Proposal
Allow a user to define a set of styles to apply to a custom element as an option to
customElements.define
. Conceptually, providing styles would make the element act as if it had ashadowRoot
including astyle
element with the provided css. The rules used to target the element would be the same as in Shadow DOM.Discussion
To style a custom element that does not otherwise need a
shadowRoot
incurs an unfortunate performance penalty and is cumbersome. A user must create a shadowRoot and put inside it astyle
element and aslot
element. Providing this styling at define time gives the platform an opportunity to optimize beyond what could be achieved when interpreting user code that installs the shadowRoot, style, and slot at construct/connected time. In addition, because the proposed syntax is less code than the alternative, it would likely reduce concern over https://github.com/w3c/webcomponents/issues/426.Ideally, developers could include
styles
targeting elements inside Shadow DOM in addition to the element itself. Again, the providedstyles
would act as if they were in astyle
that was the first element inside theshadowRoot
. This could help address https://github.com/w3c/webcomponents/issues/282 and the feature could be explained as a constructable stylesheet when this feature is added to the platform.