Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
sorvell opened this issue Mar 29, 2016 · 220 comments
Closed

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

sorvell opened this issue Mar 29, 2016 · 220 comments

Comments

@sorvell
Copy link

sorvell commented Mar 29, 2016

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 #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 #282 and the feature could be explained as a constructable stylesheet when this feature is added to the platform.

@domenic
Copy link
Collaborator

domenic commented Mar 29, 2016

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
Copy link

+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
Copy link
Contributor

See also #376 (closed), where I had a similar idea.

@andyearnshaw
Copy link

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
Copy link

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
Copy link

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
Copy link
Contributor

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.

  • In default, a style only applies to the custom element itself.
    • That means "*" only matches the custom element itself in this context.
  • Allow only a compound selector to avoid the wrong usage.
    • e.g. "div > *" is invalid.
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
Copy link

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
Copy link
Contributor

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
Copy link
Collaborator

annevk commented Apr 6, 2016

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
Copy link

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
Copy link
Collaborator

rniwa commented Apr 6, 2016

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

@tabatkins
Copy link

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

@domenic
Copy link
Collaborator

domenic commented Apr 8, 2016

@sorvell, does @hayatoito's simpler approach in #468 (comment) 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
Copy link

Yeah, def.

@sorvell
Copy link
Author

sorvell commented Apr 8, 2016

@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
Copy link

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
Copy link
Contributor

trusktr commented Apr 26, 2016

@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
Copy link
Contributor

trusktr commented Apr 26, 2016

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
Copy link

nazar-pc commented Apr 26, 2016

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
Copy link
Contributor

trusktr commented Apr 26, 2016

@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
Copy link

@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
Copy link
Collaborator

domenic commented Apr 26, 2016

@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
Copy link

@rakina
Copy link
Member

rakina commented Oct 18, 2018

But that's not how stylesheets would work in a Shadow root. If you had a #host selector that hypothetically matched the host, then it would override any :host selector, as it'd be more specific.

@emilio Yeah I should have been more clear, in the example case since there is no specificity difference then it is resolved by the order. In the case you mentioned where the default style has higher specificity than styles in the shadow root, the default style would win.

What's the specificity of the selectors in that stylesheet expected to be? Is it cascaded in a separate step instead?

I'm not sure I understand your first question, but it is using the shadow cascading order, so default style is in the same step as shadow styles.

@emilio
Copy link

emilio commented Oct 18, 2018

I'm not sure I understand your first question, but it is using the shadow cascading order, so default style is in the same step as shadow styles.

Your first reply answered my question, thanks! :)

So specificity just works normally... which means that ShadowRoot !important styles may not override these? That seems somewhat unfortunate, since that was one of the reasons why the cascade in Shadow DOM works the way it does AFAIK... Though I guess it's the side effect of not adding a new cascade order.

@tabatkins
Copy link

An alternative is to go ahead and put these in the UA origin, but disallow !important (so you can't put totally un-overridable styles in there). This would feel more like the default styles from the UA stylesheet that all the built-ins have.

@rniwa
Copy link
Collaborator

rniwa commented Oct 18, 2018

But then the author of a component can't specify !important rules for things like display without which the component may not function. This is a deal break for us.

@hayatoito
Copy link
Contributor

hayatoito commented Oct 23, 2018

I think it would be worth reconsidering the cascading order of "Custom Element Default Style" of the proposal.

Currently, it is defined as:

The default styles will be treated as if they are the first stylesheets in the custom element's shadow root's stylesheets, if exists

however, we've seen several feedback arguing this rule is unfortunate. Given that, we might want to make it better so that web developers wouldn't encounter specificity issues.

Remember that there are also several requirements mentioned in this thread, as such:

Requirement 1:

But then the author of a component can't specify !important rules for things like display without which the component may not function. This is a deal break for us.

Requirement 2:

I don't think it's okay to provide web page authors a mechanism to set a style user (not UA) stylesheet can't override. That would be a serious degradation for end users who need to use user (not UA) stylesheet to adjust the visual appearance of websites for accessibility purposes.

To make it better and satisfy these requirements at the same time, how about adding new origin, "custom elements default style declarations" to origin and Importance, as follows?

  1. Transition declarations [css-transitions-1]
  2. Important user agent declarations
  3. Important user declarations
  4. Important custom elements default style declarations
  5. Important author declarations
  6. Animation declarations [css-animations-1]
  7. Normal author declarations
  8. Normal custom elements default style declarations
  9. Normal user declarations
  10. Normal user agent declarations

Then, I think we can satisfy requirements:

  • "4. Important custom elements default style declarations" wins over "5. Important author declarations", which satisfies Requirement 1.
  • "2. Important user agent declarations" wins over "4. Important custom elements default style declarations", which satisfies Requirement 2.

Blink is interested in implementing this idea, adding this new origin, "Custom elements default style declarations", and would like to see how things are going well and get feedback from web developers.

We are aware that there was an objection to adding new origin, however, we couldn't think of any other ideas, as of now.

@rniwa
Copy link
Collaborator

rniwa commented Oct 26, 2018

TPAC F2F: There is no agreement on the latest proposal in #468 (comment) because of Apple's concern for the developer ergonomics and performance impact. Google offered to conduct an origin trial to gather more developer feedback.

@rniwa
Copy link
Collaborator

rniwa commented Nov 6, 2018

Again, I can't emphasize enough our objection for using a new cascading order.

This feature was originally proposed as a way of avoiding the performance penalty to have a shadow root. Introducing a new cascading order would totally defeat that performance benefit by introducing more complexity and performance cost in our implementation.

In general, this issue has morphed into a feature which lacks a clear list of use cases and instead of a solution looking for use cases to back that up. We need to re-enumerate a clear list of concrete use cases this feature is supposed to address since the proposed solution and the original problem make no sense whatsoever at this point.

@calebdwilliams
Copy link

calebdwilliams commented Nov 6, 2018

Is there any real benefit to this feature over just pushing a sheet into document.adoptedStyleSheets (assuming that feature gets finalized and adopted)? If not, why not just defer to that proposal? Ostensibly the best way to create an otherwise inert stylesheet would be using the features already being discussed around adoptedStyleSheets.

EDIT
Could this be sugar for adoptedStyleSheets that would push the host styles into whatever DocumentOrShadowRoot a given element becomes connected in? Not sure how the resolution would work not knowing tag names unless there's some magic involved. Just thinking out loud.

@hayatoito
Copy link
Contributor

Thanks.
Style rules added in document.adoptedStyleSheets are only applied to an element in a document tree.
Custom elements in other trees, e.g. shadow trees, wouldn't get styled.

We have to add style rules to every node trees' adoptedStyleSheets, which is impractical, I think. The author of custom elements can't control how they are used and where they are used. If the user of custom elements forget to add style rules to some node tree's adoptedStyleSheets, the custom element in the tree wouldn't get styled.

For the latter part, #769 could be related.

@andyearnshaw
Copy link

@rniwa

But then the author of a component can't specify !important rules for things like display without which the component may not function. This is a deal break for us.

Could you clarify the use case for a component author overriding display and other rules? It is unthinkable to me that a component consumer should be blocked from using display: none.

@rniwa
Copy link
Collaborator

rniwa commented Nov 28, 2018

But then the author of a component can't specify !important rules for things like display without which the component may not function. This is a deal break for us.

Could you clarify the use case for a component author overriding display and other rules? It is unthinkable to me that a component consumer should be blocked from using display: none.

For example, some components may not function when display is inline because it assumes the block layout to be happening in its shadow tree. As another example, certain component may not support vertical writing mode. e.g. input, textarea, etc... don't automatically inherit its parent writing mode; in that case, being able to force a horizontal writing mode on the component's shadow tree is crucial.

@andyearnshaw
Copy link

I suppose the writing-mode example makes sense, but I feel like !important is not the correct mechanism for this kind of thing. If I add display: inline to a <textarea> element, it's true that this style will be ignored and the computed style will be inline-block, but I can still use display: none, which is an age-old method of hiding elements and preventing them from affecting layout.

@trusktr
Copy link
Contributor

trusktr commented Jan 23, 2019

@JanMiksovsky

Generally, I think it's going to be very unusual to attach a shadow root to an element you don't define.

All of my custom elements except for one, have styles but no shadow roots and do not distribute any children. Currently I'm applying styles using JSS directly to all of their style="" attributes (because otherwise they don't reach into shadow roots).

Applying these styles via the custom element definition would be great!

@trusktr
Copy link
Contributor

trusktr commented Jan 28, 2019

Now that I think about it, I think something like a static stylesheet property in the element's class definition would be nicer, and makes more sense when thinking about which styles a custom element class inherits from a parent class (f.e. it makes sense that classes extending HTMLDivElement would have display: block by default, while classes extending HTMLSpanElement would have display: inline by default).

@andyearnshaw
Copy link

@rniwa in order to solve the override problem, what if a new CSS function were introduced to allow an author to define permitted styles for an element? e.g.

:host {
    display: allow(block, inline-block, none);
    writing-mode: allow(horizontal-tb);

    /* special interaction with minmax() ? */
    --column-width: allow(minmax(10px, 300px));
}

The function could be special-cased to only be valid in the UA origin (to be compatible with @tabatkins's suggestion of making the styles work like UA defaults), but override even !important declarations where the property is not one of the permitted values. The first value in the list would be the default.

This way, you don't need a new stacking order and you can define a subset of permitted values for the props that matter. It would also provide reasoning for UA elements that behave this way already, such as setting display: inline on a <textarea> as I mentioned before, and would improve tooling by, for example, only showing those permitted values when interacting with the properties in the dev tools.

@trusktr
Copy link
Contributor

trusktr commented Feb 1, 2019

@sorvell FYI I downvoted your original post not because I don't like the idea, but because I think that passing class-specific stuff into a non-class API seems funky. Seems like we should take advantage of mechanisms that class provides because we're using it (re: my previous comment).

@alkismavridis
Copy link

alkismavridis commented Jan 17, 2020

I think it would be cool to give us the option to include multiple stylesheets on a web component.

I can imagine a usecase where someone would want to include a css library + some custom rules on his web component. Lets say I create a web-component library and I want to use bootstrap + my own project-wide stylesheet.

@justinfagnani
Copy link
Contributor

@alkismavridis that's possible via the ShadowRoot.adoptedStyleSheets property in Blink, or whatever replaces it when Safari and FF implement (it'll be a bit different).

@mzeiher
Copy link

mzeiher commented Jan 20, 2020

@justinfagnani is there a resource/link where the successor or changes to adoptedStyleSheets are discussed? we also use it heavily in our code and I want to stay up to date :)

@web-padawan
Copy link

@mzeiher FYI, there is a recent update here WICG/construct-stylesheets#45 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests