WICG / webcomponents

Web Components specifications
Other
4.36k stars 370 forks source link

Provide a lightweight mechanism to add styles to a custom element #468

Closed sorvell closed 1 year ago

sorvell commented 8 years ago

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 a shadowRoot including a style element with the provided css. The rules used to target the element would be the same as in Shadow DOM.

customElements.define("cool-element", CoolElement, {styles: ':host {display: block; }');

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 a style element and a slot 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 provided styles would act as if they were in a style that was the first element inside the shadowRoot. 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.

domenic commented 8 years 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.

tabatkins commented 8 years ago

+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.

hayatoito commented 8 years ago

See also https://github.com/w3c/webcomponents/issues/376 (closed), where I had a similar idea.

andyearnshaw commented 8 years ago

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.

tabatkins commented 8 years ago

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.

tabatkins commented 8 years ago

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.)

hayatoito commented 8 years ago

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.

tabatkins commented 8 years ago

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.)

hayatoito commented 8 years ago

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.

annevk commented 8 years ago

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.

tabatkins commented 8 years ago

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:

  1. Custom elements need some equivalent of "user agent styling", to set up how they look by default; at minimum, they need to be able to set 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.
  2. Custom elements with shadows need a way to style all of their contents.
  3. Authors using custom elements need a way to style them.
  4. Maybe need some way to style all instances of a particular element in the page the same way? Like make all buttons look the same.

1 is this proposal - attach some styles to the element at registration time, they're treated as user-agent origin.

2 is stylesheets in the shadow root.

3 is custom properties, applied via var() and @apply.

4 I'm not sure about yet, but Hayato's idea addresses it.

rniwa commented 8 years ago

Which idea of Hayato are you referring for (4)?

tabatkins commented 8 years ago

The one that he and I were talking about immediately prior to this, from issue #376.

domenic commented 8 years ago

@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.

tabatkins commented 8 years ago

Yeah, def.

sorvell commented 8 years ago

@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.

nazar-pc commented 8 years ago

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.

trusktr commented 8 years ago

@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})
trusktr commented 8 years ago

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?

nazar-pc commented 8 years ago

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).

trusktr commented 8 years ago

@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
tabatkins commented 8 years ago

@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.

domenic commented 8 years ago

@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?

tabatkins commented 8 years ago

https://drafts.csswg.org/css-syntax/#parse-stylesheet

rniwa commented 8 years ago

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?

justinfagnani commented 8 years ago

I think there are two main cases:

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?

domenic commented 8 years ago

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:

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?

rniwa commented 8 years ago

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?

tabatkins commented 8 years ago
  • 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.

rniwa commented 8 years ago
  • 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.

travisleithead commented 8 years ago

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?)

trusktr commented 8 years ago

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.

trusktr commented 8 years ago

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>?

domenic commented 8 years ago

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.

trusktr commented 8 years ago

@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?

domenic commented 8 years ago

Correct.

hayatoito commented 8 years ago

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.

tabatkins commented 8 years ago

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().

trusktr commented 8 years ago
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
);
matthewp commented 8 years ago

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.

WebReflection commented 7 years ago

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?

As 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.

TakayoshiKochi commented 6 years ago

FYI - I posted a thread about constructable stylesheets at WICG discourse https://discourse.wicg.io/t/proposal-constructable-stylesheet-objects/2572

tomayac commented 6 years ago

@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.

calebdwilliams commented 6 years ago

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.

rniwa commented 6 years ago

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.

calebdwilliams commented 6 years ago

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']
});
trusktr commented 6 years ago

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.

trusktr commented 6 years ago

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.

calebdwilliams commented 6 years ago

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.).

calebdwilliams commented 6 years ago

@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)?