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

Web components should be able to easily adapt to the host page while maintaining enapsulation #986

Open
LeaVerou opened this issue Feb 27, 2023 · 48 comments

Comments

@LeaVerou
Copy link

LeaVerou commented Feb 27, 2023

This originates in this Twitter thread and this Mastodon thread.

Problem statement

Components for encapsulating and packaging up blocks of functionality are usually composed from built-in controls, such as buttons and inputs, often recursively.
Host pages nearly always include base styles for all built-in controls, yet these components cannot take advantage of these styles without placing their elements in light DOM, which breaks encapsulation entirely. Furthermore, there is not even a straightforward way (either in CSS or JS) for these components to pull in styles from the shadow host.

As a result, it is currently a nontrivial problem to create a web component that e.g. contains a text field and a button, and have that text field and button look like the other text fields and buttons on the surrounding page.

Yes, there is ::part() and custom properties, but this means the host page needs to know which parts and custom properties the component uses, so currently authors need to spend a considerable amount of time integrating a component in their page, which makes it harder to mix and match components, and promotes monolithic component libraries that share the same styling conventions. There have been some efforts to standardize styling conventions, but nothing has caught up.

This also affects built-in components as well: e.g. <input type=file> has historically been annoying for authors because even though it includes a button, the button does not follow the page's button styles and needs to be styled separately.

image

or even the arrow buttons in <input type=number>:

image

This also comes up for non-interactive components: e.g. there are plenty of components for including Markdown in an HTML page, all suffering from the same problems: either they render Markdown in Shadow DOM and thus it looks out of place unless the page author spends a considerable amount of time duplicating their core styles for parts (assuming everything is actually exposed via parts), OR everything is rendered in the light DOM so it can be styled as normal, which means the original Markdown code is lost after the first render.

There is the old open-stylable Shadow Roots proposal, but I think for most use cases component authors need more control than indiscriminately pulling in all styles from the host page.

Potential solution

Essentially what we need is a declarative way for component authors to selectively opt-in controls to be stylable from the shadow host's styles, with certain restrictions so that you end up essentially only getting global styles intended for these controls (rules like button, button:hover, input[type=number], details > summary, [popover]), and not random ad hoc CSS code that would cause styling conflicts. I think the following restrictions would achieve that:

  • Obviously, selectors involving combinators with unexposed parts of the shadow tree should not match, and selectors cannot cross shadow boundaries to match.
  • Certain attributes cannot take part in matching: id, class, part, data-* (any others?)
  • Only user action pseudo-classes and linguistic pseudo-classes can take part in matching, not structural pseudo-classes.
    • We don't want things like :nth-child() to match, do we? Unless we want to be able to pull in styles for more complicated structures like entire tables — do we want to cover this type of use case?

The opt-in mechanism would likely be an attribute, name TBB. Some (poor) ideas: hoststylable, importstyles, allowhoststyles. Hopefully we can find something shorter than these. The idea being that the attribute would start as a boolean attribute, but could in the future be expanded to take values for customizing the behavior.

Open questions

  • How does this attribute work across nested shadow roots? Do we need an explicit opt in (akin to exportparts) or do we just define it so that matching styles from all parent shadow hosts apply? It seems like the latter could better preserve intent.

I'd love to hear from implementors: would something like this be feasible? How much implementation effort would it require? Are there any changes that would improve implementability?

@rniwa
Copy link
Collaborator

rniwa commented Feb 27, 2023

See also: #909

@zachinglis
Copy link

Trying to adhere to the way native components works is possibly one of the best solutions. Where outside specificity easily overrules component-level styles.

I think a main problem occurs because custom web components generally are far more complex and deeper than form controls and the like. They end up involving a whole series of children. Native components involve 1 text style or 1 background style more often than not. Whereas custom webcomponents can easily see 2 type/color treatments. While the goal is to not style components, the user of a component may have special em styling or such.

I think webcomponents having weaker specificity by default, allowing things to overridden would be good. That will involve some unintended side effects when implementing some in your code. But I also think anyone who implements one will see that straight away.

The other side is more easily invoking the shadow DOM later in the chain. Rather than it being binarily on, encapsulating everything, or not on at all.

@DarkWiiPlayer
Copy link

At work we just have a snippet that ends up being copied into nearly every web project that essentially just loops over document.stylesheets and cloneNode()s them into the elements shadow-dom. It's by far the more common use-case for us to want the shadow-dom to encapsulate behaviour but not styles. Cases where a custom element is so visually unique that it needs its own complete styling are very rare in practice.

@LeaVerou
Copy link
Author

LeaVerou commented Feb 27, 2023

@rniwa

See also: #909

Erm, I am linking to this in my first post:

There is the old open-stylable Shadow Roots proposal, but I think for most use cases component authors need more control than indiscriminately pulling in all styles from the host page.

@zachinglis

Trying to adhere to the way native components works is possibly one of the best solutions. Where outside specificity easily overrules component-level styles.

Not sure what you mean. Outside specificity does not overrule anything in shadow dom in native components.

I think a main problem occurs because custom web components generally are far more complex and deeper than form controls and the like. They end up involving a whole series of children. Native components involve 1 text style or 1 background style more often than not. Whereas custom webcomponents can easily see 2 type/color treatments. While the goal is to not style components, the user of a component may have special em styling or such.

There are some pretty complex native elements, e.g. <video controls>.

I think webcomponents having weaker specificity by default, allowing things to overridden would be good. That will involve some unintended side effects when implementing some in your code. But I also think anyone who implements one will see that straight away.

This is not a specificity problem at all. I think you may be referring to another issue (potentially the problems discussed in w3c/csswg-drafts#7922 ?).

The other side is more easily invoking the shadow DOM later in the chain. Rather than it being binarily on, encapsulating everything, or not on at all.

I think this is the point of #909.

@DarkWiiPlayer

At work we just have a snippet that ends up being copied into nearly every web project that essentially just loops over document.stylesheets and cloneNode()s them into the elements shadow-dom. It's by far the more common use-case for us to want the shadow-dom to encapsulate behaviour but not styles. Cases where a custom element is so visually unique that it needs its own complete styling are very rare in practice.

I think this use case is actually better served by open-stylable shadow roots, see #909

@DarkWiiPlayer
Copy link

Just to throw this idea out:

<my-component>
   <template shadowrootmode="open">
      <style>
         /* Basic case */
         button { all: outside }
         /* Inside of the form element, just pull in outside styles, like if it was slotted */
         @scope (form) to (whatever-nested-component) {
            * { all: outside }
          }
          a { color: red; font-size: outside; }
      </style>
   </template>
   <button>Do Things</button>
   <a class="fancy-link">Wouldn't want ANY outside styles messing with this super complex link styling (Except the font size, that's fine)</a>
   <form>
      <!-- some form controls here -->
   </form>
</my-component>

Needless to say, outside is just the first thing that came to mind, but that's one way I could imagine this to work that would be comfortable in my typical use-cases.

@LeaVerou Copying in all styles has had its problems here and there and we've had to find many work-arounds, which would still be the case with open-stylable. That one seems like a good first step, but I also think a more fine-tunable solution would be best.

@zachinglis
Copy link

zachinglis commented Feb 27, 2023

Apologies. I think I didn't write clearly and it ended up a bit of a word salad. Hopefully I explain myself better here.

Not sure what you mean. Outside specificity does not overrule anything in shadow dom in native components.

In a lot of native controls, such as inputs/selects/etc, there is decent user styling because they are more granular, and the wrapper does a lot of heavy lifting. The more complex, the harder it is obviously to do. (And correct me if I'm wrong, as I've only created a few dozen web components, and you're in this group,) but most people's use cases tend to involve a deeper hierarchy than those.

There are some pretty complex native elements, e.g.

Right. That's why I was saying 'generally'. Video controls also lacks much customisation in terms of CSS control, nor is there much you would want to inherit naturally. MDN tells you to dive into the Shadow DOM, or completely roll your own.

In those regards, my point was meant to be; the ability to turn off authored styling (akin to appearance: none may help solve some of the issues. When I've built web components before, I've also often wished to inherit some rules, and override others. That's why I think a combination of exposing only some is definitely the right way to go but also not others, as you said. (It was more meant to be in agreement, than a counter point.)

I like those links, thank you.

I think this is the point of #909.

I think this would solve some of these problems. But as you said, there will be conflicts still.

I like your solution. Did you imagine it being able to cascade at all? I worry about the repetitive nature if you wanted to expose 60% of your component to allow styling. I could see myself allowing at the top level, and then disallowing at certain points.

Certain attributes cannot take part in matching: id, class, part, data-* (any others?)

I worry not allowing granularity (such as classes) may not fix some of the issues. Inputs, Buttons, etc often have class based alternatives. (Obviously this should be mostly solved on attr level if possible. But they're not always)

@AmeliaBR
Copy link

Some thoughts & questions (adapted from mastodon discussion):

  • IMO, there should be two-way opt-in for style crossover. The web component author would need to explicitly pull in external styles in the CSS, or somehow make the elements visible to external selectors (with element attributes or settings used when adding the shadow tree). But the page author should also need to enable the style crossover, either with an attribute on the embedding custom element, or a JS option when registering the custom element class. (Either of which is still a lot easier than re-writing your full stylesheets to use custom properties or ::part selectors.) For nested web components, there would need to be opt-in all the way up the tree.

  • Selectors are used for more than just style rule matching. Is there a benefit or risk in also opting-in to visibility for querySelector (and similar)?

  • Is it enough to just say that, for example <button> within the shadow tree should match a button selector from the light(er) DOM, or should there be a way to say that <my-button> (that is, a nested custom element) or a <div role="button"> should appear to be a button for the purpose of the host document's stylesheets?

  • This feature needs to avoid naming clashes for classes and data-attributes (i.e., the same class name used in the external page for a different meaning than how it is used by the web component author; this is the main reason for wanting encapsulation of stylesheets). But for the proposal to be useful, it would need to support a full set of selectors based on HTML semantics (type attribute, aria, pseudoclass / pseudoelement). Is it realistic to define a global allowlist / blocklist of visible attributes? Or would the web component author need to configure this somehow?

    Similarly: lots of typographic and form styles depend on sibling or parent/child relationships, so you'd want to support complex selectors. But this breaks the encapsulation of the shadow DOM tree, and could create brittle dependencies where an extra wrapping div or span, or a nested web component or slot element, would throw off the styles.

  • I know there has been some spec discussion related to other named items in stylesheets (imported fonts, animation keyframes, and custom properties) and how they are interpreted when styles inherit across shadow boundaries. I don't know whether that's all been specified yet, but this would make it all the more urgent that it is cleared up in a way that avoids unintentional name clashes.

  • How would / should the CSS cascade combine shadow/light(er) DOM style rules on a single element? Do the existing rules around slotted and host pseudo selectors make sense if generalized, or would additional cascade rules be required?

  • Do existing rules about selector matching (on light and shadow DOM trees separately) vs style inheritance (on the slotted tree) still make sense for the desired use cases?

To answer many of these questions, it would help to have a collection of use cases for web components with a mix of slotted and templated elements where styling should be harmonized with the parent document, and examples of how WC authors are currently handling it (by building up templates all in the light DOM, or by copying stylesheets into the shadow DOM, or using complex custom props systems, or ???).

@Westbrook
Copy link
Collaborator

Westbrook commented Feb 27, 2023

You've proposed an allow list approach here, but what if there was a block list approach? It would seem that you might be able to achieve that assuming a version open-stylable Shadow Roots proposal that allowed appending style for just the shadow root and !important. That way you opting out of styles from the host in certain places. !important is a bit of concerning API for some, but it seems to be coming back into vogue with the clarity that @layer brings to the technique, so maybe it's the a good path here?

An open-stylable Shadow Root (so you get <slot> and protection from JS selectors, that had default styles at ::slotted() specificity, but could accept !important specificity sounds like it could be pretty interesting.

@PaulHMason
Copy link

PaulHMason commented Feb 28, 2023

I would be happy if I could opt in to external native form element styling by simply setting an attribute on the elements within the shadow root. Something like <button allow-external-styles></button>. That would limit external style matches to button {...} only, but I think that's fine because I don't see much sense in going crazy with different styles for different shadow dom buttons - the idea is consistency. If people want to get really creative then there's always part.

Another option could be something like a button::shadow selector that pierces the shadow boundary for form elements. That way web component developers don't need to do anything special, but consumers mess with the form elements at their own risk.

Unrelated, but more important to me - being able to style all aspects of the various input elements.

@naiyerasif
Copy link

Aside from web component authoring perspective, I'd also suggest some affordances from an end-user perspective (as in the user of a web component provided by a library): an API to enable this at runtime. It can be an attribute as suggested by @PaulHMason, for example, <command-bar style-mode="inherit-host"></command-bar>

This can help me, as a user, to use a web component with global overrides if needed and without if not needed.

@LeaVerou
Copy link
Author

Some thoughts & questions (adapted from mastodon discussion):

  • IMO, there should be two-way opt-in for style crossover. The web component author would need to explicitly pull in external styles in the CSS, or somehow make the elements visible to external selectors (with element attributes or settings used when adding the shadow tree). But the page author should also need to enable the style crossover, either with an attribute on the embedding custom element, or a JS option when registering the custom element class. (Either of which is still a lot easier than re-writing your full stylesheets to use custom properties or ::part selectors.) For nested web components, there would need to be opt-in all the way up the tree.

I strongly disagree.

  • If a component is written assuming form controls get styles from the surrounding page, it would not work well without that. As a WC author, you don't want to write even more code to account for both cases.
  • What happens when the attribute is used externally, but the component doesn't use the corresponding attribute internally? Poor DX.
  • It also increases the boilerplate authors have to write to use the element.
  • This means that in practice this attribute wouldn't be used, and developers would instead do things such as @DarkWiiPlayer described of pulling in all styles from the shadow host.

As a WC author, you want your component to Just Work™, without people using it having to jump through hoops. It's far preferable to shift complexity to the component internals, than outwards to the component consumer.

  • Selectors are used for more than just style rule matching. Is there a benefit or risk in also opting-in to visibility for querySelector (and similar)?

I don't see the point. WIth open shadow roots, authors can always access whatever they want through element.shadowRoot, so this adds extra complexity without really serving any more use cases.

  • Is it enough to just say that, for example <button> within the shadow tree should match a button selector from the light(er) DOM, or should there be a way to say that <my-button> (that is, a nested custom element) or a <div role="button"> should appear to be a button for the purpose of the host document's stylesheets?

That's a good point, and indeed a syntax that allows for this too would be great. Though I worry that trying to cater to those use cases as well would overcomplicate this, or prevent the syntax from catering to other use cases (e.g. one can imagine a syntax where you explicitly specify what element types and attributes you want this to match as, but then you lose combinators etc). I'd also imagine that having a <my-button> that does selector matching as a <button> would be rather complex to implement.

  • This feature needs to avoid naming clashes for classes and data-attributes (i.e., the same class name used in the external page for a different meaning than how it is used by the web component author; this is the main reason for wanting encapsulation of stylesheets). But for the proposal to be useful, it would need to support a full set of selectors based on HTML semantics (type attribute, aria, pseudoclass / pseudoelement). Is it realistic to define a global allowlist / blocklist of visible attributes? Or would the web component author need to configure this somehow?

This is why my proposal was to exclude certain "userland" attributes from matching. We definitely don't want to be maintaining allowlists of attributes in the spec though, that's a recipe for disaster.
Not sure all pseudo-classes would be useful, e.g. :nth-child() would reveal things about the element's position in the DOM that you may want to encapsulate. That's why I proposed specific categories of pseudo-classes. I should also add input pseudos.
Pseudo-elements would be useful too, e.g. you probably want things like ::placeholder, ::selection etc.

Similarly: lots of typographic and form styles depend on sibling or parent/child relationships, so you'd want to support complex selectors. But this breaks the encapsulation of the shadow DOM tree, and could create brittle dependencies where an extra wrapping div or span, or a nested web component or slot element, would throw off the styles.

Per my original proposal, combinators would match as long as all the elements involved were exposed. E.g. details > summary would match as long as both <details> and <summary> were opted in through this attribute.

  • I know there has been some spec discussion related to other named items in stylesheets (imported fonts, animation keyframes, and custom properties) and how they are interpreted when styles inherit across shadow boundaries. I don't know whether that's all been specified yet, but this would make it all the more urgent that it is cleared up in a way that avoids unintentional name clashes.

Ah, that's a good point, right now these are tree scoped, i.e. animations from the outside do not trickle in.
I don't see an obvious way to resolve this without pulling in all the names (which is what open-stylable would do).
Though I think even if they continue to be private, there's usefulness in this.

  • How would / should the CSS cascade combine shadow/light(er) DOM style rules on a single element? Do the existing rules around slotted and host pseudo selectors make sense if generalized, or would additional cascade rules be required?
  • Do existing rules about selector matching (on light and shadow DOM trees separately) vs style inheritance (on the slotted tree) still make sense for the desired use cases?

Not sure what you mean in either of these questions.

To answer many of these questions, it would help to have a collection of use cases for web components with a mix of slotted and templated elements where styling should be harmonized with the parent document, and examples of how WC authors are currently handling it (by building up templates all in the light DOM, or by copying stylesheets into the shadow DOM, or using complex custom props systems, or ???).

Good point, I'll try to gather some. FWIW what prompted this was me writing a component for image input (that supported both linking, uploading, pasting etc) and I wished the input and button could automatically get styling from the host page.

I think the way most WC libraries deal with it is that they include their own core styles, and try to offer parts and custom properties liberally, for customization. Which of course means a lot of integration effort for the WC consumer.

@bradkemper
Copy link

bradkemper commented Feb 28, 2023

My dream for this issue is to have a :button pseudo class that selected button, input[type=button], input[type=submit], etc. (did I miss any?) in the light DOM, and also pierces component shadow boundaries to select anything the component author decided should also receive button styling, such as by adding a getsButtonStyling attribute or something. Thus, if the button wasn’t supposed to look like a regular button, they just wouldn’t opt it in with that attribute.

Then, besides :button, there could also be :text-field, :radio, :checkbox, :label, etc.

@caridy
Copy link

caridy commented Mar 1, 2023

From my recollection of the many discussions about Web Components with some google folks many years ago, there was one idea that hunted me for years, it was @domenic challenging the presumption that web components were meant to be used to encapsulate complex logic, specifically, multiple layers (logical layers - (e.g.: a gallery using a gallery item on its shadow, or a product set, containing a product price on its shadow). He kept saying, "don't do that", and of course, we didn't listen. It took us years, it took me years I should say, to realize what he was saying, and more important, to realize that this is the root of many problems for Salesforce ecosystem of Web Components. If you fall into that trap, then all the problems that you're describing will emerge immediately.

My recommendation is: "Listen to @domenic", it is somehow counter intuitive because most of us are coming from different frameworks, and mostly trying to adapt to the WC model, so it is very tempting. Here are some of the rules that you can reasoning about:

  1. build building blocks that encapsulates logic and UI elements that are "fully" customizable by using existing mechanisms (css properties, parts, slots, etc.). everything most be customizable from outside the shadow.
  2. if a building block is using another building block on its shadow, it must do it as part of the default content of a well defined slot. <-- this is extremely important, otherwise you're going to violate 1.
  3. only accept primitive values as properties and attributes.
  4. don't be afraid of creating a big/large HTML with many composable building blocks to construct your experiences, you don't necessary need a shadow at that point, and you will have a single hump to style/customize any part of such large html fragment.

I suspect that we, as a community, need to do a better job communicating and perhaps be more prescriptive, certainly implementers will not do it, but I see more and more developers falling for this trap, and at the end it is not helping the WC cause. Trying to supplement WC with new APIs to allow you to customize encapsulated components will never be good enough IMO, because the problem is the model you're aiming for, not necessary the WC API.

@domenic
Copy link
Collaborator

domenic commented Mar 1, 2023

I haven't been able to understand all the proposals or ideas in this thread, but I will briefly say that this is part of something I've been thinking about writing up recently. Which is that, basically, web components were designed from the beginning to give the same styling capabilities as native controls like <input type=date>, <details>, <video controls>, etc., but that's not enough power for how people want to use them.

I think @caridy summarizes this well, but I'll give my own take on what "the same styling capabilities as native controls" means:

  • All interesting content (e.g., something a search engine might care about) should be in the light DOM; shadow DOM should only contain encapsulated "decorations". (Decoration examples: month/day/year separators, date-pickers, ">" markers for <details>, <video>'s controls, etc.)
  • Those encapsulated decorations can be styled in limited ways, using ::part() and CSS variables.
  • Default styles on the light DOM content are not very significant, often nonexistent. (E.g. look at the default styles for <input> or <fieldset>/<legend>.)

This is what led me to invest a lot of time in web component improvements like default accessibility semantics, form-associated custom elements, CSS shadow parts and custom state, etc. (We still haven't figured out default focus though...)

In practice, I think this is rarely how we've seen people using web components. (Despite, maybe, @caridy and his team coming around to this point of view? 🙂) People want to use them as a full "component model" in the same style as React and friends. This leads to a bunch of new proposals like declarative shadow DOM, open-stylable shadow roots, scoped custom element registries, deferred upgrades, etc. In my view all such proposals are part of making them better as a React-competitor component model.

Where does that leave us? Well, I've come to peace with the idea of building things developers want (a full component model), instead of saying that you're only able to use web components fruitfully if you are building native-HTML-like elements. I still think it's an elegant vision to build out a suite of technologies such that you build new HTML-ish generic elements, with their interesting content in light DOM and their decorations selectively exposed as ::part()s etc. But that's not really what developers want to do. They instead want to build <mycorp-button> or <mysite-sidebar> or <myapp-widget>. (E.g. look at the React components MDN recommends creating for a todo-list app.) And so maybe it's fine to build tech to help with that, even if it's retrofitting on top of a base web components architecture that wasn't really designed for it.

@dbatiste
Copy link

dbatiste commented Mar 1, 2023

As someone on the app side of things, I can say we have lots of low level web components within our application, and we also have lots of app level web components that to varying degrees are internally composed of lots of other components. We went all in, so we aren't faced with these styling constraints to the same extent, but I understand that it does block adoption for others. More often, we are limited by third-party integrations that have not considered DOM encapsulation by relying on global styles or assuming everything is in the same scope.

For a long time people wanted the ability to define their own elements, and web components meets that need in a standard way supported natively across all current major browsers - no special flavour of the month libraries required. Some of those app level components are shared across the app and potentially used in different contexts. All of our teams, partners, or super-users can build for our app provided they're using tech that is compatible with web components, and we don't need to force them to use specific version of a component library that we may or may not want to support tomorrow. The last thing I would want is to use flavour of the month component library for app components. I know React isn't exactly flavour of the month, but we've been bitten by incompatible versions with it nonetheless. So while there are gaps with web components, my hope is that we would continue to strive forward and make it better for building apps.

@LeaVerou
Copy link
Author

LeaVerou commented Mar 1, 2023

@caridy @domenic It's inaccurate (even a bit dismissive) to say that everyone who has been running into these problems is trying to use WC for templating, framework-style. I've been on the side of building WC as native-like HTML elements for a while — these types of components are literally 99% of the components I have ever built — so you are preaching to the choir here. I’ve been pretty vocal about how it was a mistake that style ended up being baked in to WC libraries — instead they should have been blocks of functionality, encapsulating only functional CSS, and entirely themable from the outside (most recent tweet on this).

And yet, I've ran into the use cases I'm describing repeatedly, while authoring WC with that mindset. As I mentioned in my first post, built-in components run into these issues as well, we've just accepted that this is how things are supposed to be.

For example, think of the "Browse…" button in most <input type=file> implementations.

image

or even the arrow buttons in <input type=number>:

image

Is the intention really for these to not inherit any <button> styles from the surrounding page, or just an artifact of how style encapsulation currently works?

Themable from the outside does not mean it's okay if authors have to repeat all their global styles for every single component they add, because it uses different parts for its buttons and inputs. Experimenting with a WC should not take that much work.

@naiyerasif
Copy link

naiyerasif commented Mar 1, 2023

Themable from the outside does not mean it's okay if authors have to repeat all their global styles for every single component they add, because it uses different parts for its buttons and inputs. Experimenting with a WC should not take that much work.

This is what's important. People try to solve a problem and a standard should help them focus on solving the problem rather than ceremony. Too much ceremony nukes the motivation to work with something and leads people to embrace other ways (that's React, Svelte, etc) to solve a problem.

@caridy
Copy link

caridy commented Mar 1, 2023

As I mentioned in my first post, built-in components run into these issues as well, we've just accepted that this is how things are supposed to be.

@LeaVerou there is no question about it, they are limiting, and I don't know exactly why, but I'm not an implementer! I will assume, that if you are implementing a <fancy-input type=number>, I will expect 2 things: a) the ability to use ::part() to style, in some degree, the arrow buttons, and b) Two slots to replace the arrow buttons all together. I will not assume that the default content of the two slots will be fully customizable from outside without replace them all together by providing content for the slots. In other words, a) from above is to get you to the "good-enough" customization that can cover majority of the cases, while b) is to get you to the "pixel-perfect" customization that is rare.

@rektide
Copy link

rektide commented Mar 1, 2023

I will expect 2 things: a) the ability to use ::part() to style, in some degree, the arrow buttons,

If we have a tree of WCs, it feels like it becomes quite a chore to go explicitly re-target all the various "arrow buttons" (or what not) throughout the tree, no? (I may be missing some option!)

b) Two slots to replace the arrow buttons all together.

This feels like it's going the opposite direction from where your previous post was, which seemed to be warding against making too many fine grained independent components: whoops we accidentally built both gallery and gallery-item, but should just have had gallery).

It feels like trying to insist each component be designed ahead of time to be fully disassembled, have to be crafted together by a lot of pieces. I actually like a lot of this notion, quite a lot, but I think it would take quite a radical turn & eternal vigilance to disassemble our WC world into small enough atoms. The idea here of components maintaining integrity but also being able to participate on the page they're on sounds like a more interesting twist, and it alleviates pressure on the page to top down define each atom, leaves WCs more able to work bottom up, which sounds like a wonderful convenience for component authors (who don't want to have to assume a blank slate input for every detail) and for page authors (who don't want to have to hand assemble most of the things inside the WC tree).

@jimmyfrasche
Copy link

@LeaVerou

Yes, there is ::part() and custom properties, but this means the host page needs to know which parts and custom properties the component uses[…]

Tangential, but it would be great if all of these would just show up in dev tools somehow, even if it meant the author having to enumerate in some way the parts/properties to expose.

@caridy
Copy link

caridy commented Mar 2, 2023

I will expect 2 things: a) the ability to use ::part() to style, in some degree, the arrow buttons,

If we have a tree of WCs, it feels like it becomes quite a chore to go explicitly re-target all the various "arrow buttons" (or what not) throughout the tree, no? (I may be missing some option!)

@kektide you are in control of the registry, or you should, if that's the case, then why do you have a fancy-input with the wrong arrow buttons? Changing the defaults of all instances of a component in the page is probably the wrong approach, just register the customized component instead. It is about customization of individual component instances.

@LeaVerou
Copy link
Author

LeaVerou commented Mar 2, 2023

@caridy:

If authors have to include manual buttons just so that these buttons are styled like the rest of their page, that does not seem like the right abstraction, nor is it particularly maintainable (the buttons may include other attributes that authors must now recreate). Including custom elements via slots should be reserved for extreme customization, not basic usage.

Similarly, parts are great, but not a good solution for just applying default, baseline styling. Imagine having to turn your button, .button selector into button, .button, ::part(lib1-button), ::part(lib2-button), .... And when using a CSS framework that's not even possible without editing framework code or duplicating it, neither of which are particularly maintainable.

@calebdwilliams
Copy link

::part could be an acceptable solution for this use case if there were a way to mix in other CSS styles either silent classes, mixins or @justinfagnani’s reference selector idea. I’d love this behavior as well and could see it as

::part(button) {
  @include $ns-button;
}

This becomes opt-in, preserves encapsulation, can be combined with other ideas like having multiple sheets exported by a single document and has user-land benefits beyond just shadow DOM.

@DarkWiiPlayer
Copy link

Correct me if I'm wrong, but that would lead us right back to the problem of using components requiring too many extra steps, would it not?

The point here is that component authors should be the only ones who have to do anything, and that it shouldn't be too inconvenient. Having to explicitly bend styles to work with component parts really defeats the purpose, specially for more complicated nested components where you'd have to list lots of boilerplate for all the different parts.

@LeaVerou
Copy link
Author

LeaVerou commented Mar 3, 2023

Yeah, I’m tending to agree with @DarkWiiPlayer: what @calebdwilliams is proposing would be slightly better than the current situation, but it has several issues when it comes to addressing the problem statement in the OP:

  • Base styling now needs to use placeholder selectors instead of normal ones
  • Glue code is still needed, it's just separate from both the component code and the base styling, and very short

@caridy
Copy link

caridy commented Mar 6, 2023

It feels that we are not speaking the same language, or that I'm not understanding very well the motivations for this request. Let me try to provide an example (similar to what I mentioned above), and you can tell us exactly that the problems are:

<body>
     ...plenty of markup... with different levels...
     <x-gallery>
         <x-gallery-image></x-gallery-image>
     </x-gallery>    
     <x-carousel>
           <x-carousel-image src="1.jpg"></x-casousel-image>
           <x-carousel-image src="1.jpg"></x-casousel-image>
           <x-carousel-image src="1.jpg"></x-casousel-image>
     </x-carousel>
     ...more markup --- all owned by document, hence no #shadowRoots here...
</body>    

We can assume that:

  1. <x-gallery> is also showing buttons to move prev/next image inside its shadow as default content for prev and next slots.
  2. all the wiring between all the elements is happening in JS (selection of images, updates of the Dom, etc).
  3. all elements in the example above belong to document, e.g.: elm.getRootNode() === document.
  4. all custom elements are exposing parts of any piece of their relevant shadow's markup.
  5. css used by the custom elements in the example above are relying on css variables/properties to control the theme aspect of them.
  6. all custom elements used by custom elements in the example above are replaceable via slots.
  7. all relevant custom elements used by custom elements in the example above are marked with parts, and are forwarding relevant parts as well.
  8. <x-carousel> can be replaced with <x-dots><x-dot>... instead, and the same principles apply for mobile.

Now, based on those assumptions, what exactly is what you're asking, and why?

@DarkWiiPlayer
Copy link

I think the main point here is that, in an ideal world, a user (who may or may not be very knowledgeable about HTML, CSS or JS) would have to a) link some style sheet with buttons and stuff and b) link some XGallery.js file from somewhere and the should be able to just throw an <x-gallery> on the page and have it Just Work™

That's the promise of custom elements: you import the JS and put the elements on your page, and most of the complexity is the responsibility of the element author. So it makes more sense to give component authors the tools to handle selectively pulling in external styles they wish to use while preventing others from messing up their components.

@calebdwilliams
Copy link

@LeaVerou I think that basic idea (of @include) could feasibly be extended to extend single selectors as well which maintains encapsulation and plays nice with other existing APIs. The entry for an inclusion API could start with something like a reference selector, but eventually be broadened to importing selector styles from the shadow host's root node.

I can envision something like the following catching on

/** Styles within a shadow DOM */
button {
  @include from-root(button); /* or */
  @include from-root(.btn);
}

@DarkWiiPlayer
Copy link

DarkWiiPlayer commented Mar 6, 2023

button {
  @include from-root(button);
}

Would this include a) any style rules from outside that have the selector button, or b) any styles that would match button regardless of what selector they sere defined with? Or something in between?

(also, I still think button { all: host } would be a more flexible alternative, if we want this to be an extension to CSS itself)

@calebdwilliams
Copy link

The way I'm thinking about it, something like from-root (which is just an idea and might not even be technically feasible) would include any CSS selector rules from the shadow-including element's root node.

all: host or all: root is clever, but not nearly as flexible, nor as broadly useful (for most of my use cases).

@DarkWiiPlayer
Copy link

I probably phrased my question a bit weird.

Say you have a document style sheet with .danger { --color: red } to mark dangerous buttons in a different colour.

In a shadow root you do button { @include from-root(button); }, would a <button class="danger"> be red, or would that rule be ignored because the selector (.danger) does not match button?

The reason I think this is important is that selectors can often get a little bit more complex in the real world and, personally, I think it would be a more common use-case to specify a selector and have all matching elements be styled "like outside", instead of white-listing specific selectors that we want to pull in.

Say for example I have a list of selectors like button, button:hover, button:focus and button.danger; the first three could somehow be defined to all get pulled in with just button, but not the last one.

I don't see much of a downside to including the .danger selector, as it could provide a mechanism for components to support certain frameworks out of the box (say you just add uk-button or whatever it's called to all of your buttons; if the host uses uikit it just works, otherwise it does nothing.)

This would also allow for more complex selectors like button:has(+ button) { /* no right margin */ } button+button { /* no left margin */ } (a simple button-group) to apply within a shadow DOM, which would otherwise require a lot of fine-tuning for what to let through.

Of course, this assumes that more purpose-specific class names won't usually clash and are given reasonable names, which might not always be the case. But this can easily be circumvented by prefixing classes inside the shadow DOM that should not pull in outside styles like <button class="gallery-danger gallery-delete uk-button">, which might be a good convention for components anyway.

@calebdwilliams
Copy link

The way I’m thinking about that option is that only the styles included get applied so if you want danger you could reapply that to any other selector from the outside in but always an explicit opt in

.uhoh {
  @include from-root(.danger);

  &:hover {
    @include from-root(.danger:hover);
  }
}

@LeaVerou
Copy link
Author

LeaVerou commented Mar 7, 2023

@caridy I think we probably have different frames of reference. E.g. a gallery is not a very good example, since it's common to want to style its controls differently from the general button styles of the rest of the page. I mentioned several examples in the OP but if you want a more concrete example, here's a more concrete example inspired from the component that finally pushed me to make an issue about this (but I have encountered the issue dozens of times while building many different types of components):

Suppose you have an <img-input> component for uploading or linking to images. The user-facing UI is an <input> (for entering URLs or editing filenames) next to a <button> (for uploading). Its markup could look something like this (simplified):

<img-input>
	#shadow-root
		<style>/* internal styles */</style>
		<div id="drop-zone" part="dropzone">
			<input id="url" part="input location">
			<div id="upload-wrapper">
				<input type="file" accept="image/*">
				<button part="button browse-button">Browse…</button>
			</div>
			<img id="preview" part="preview">
		</div>
</img-input>

with a sample rendering like this:

image

Now, you want that input and that button to be styled like every other input or button in the rest of the page, right?

  • You don't want the page author to have to reach into their styling framework and add ::part(input) or ::part(button) for that.
  • You don't want the page author to have to reflect their entire form styling into custom properties like --button-background, --button-border, --button-hover-background, --button-hover-border etc etc
  • You don't want to put these controls in the light DOM just so they are styled like the rest of the page, because you still want the encapsulation of the rest of the tree around them
  • You don't want to make these slots so that authors have to write things like:
<img-input>
	<input slot="input" />
	<button slot="browse">Browse…</button>
</img-input>

just so they can use your component, right? Boilerplate repetition is exactly what WC are trying to curb.

  • I suppose one viable solution might be to have slots like the above, and imperatively generate content for the slots if they are empty. I guess this could work but it feels like a hack, and is fairly weird.

@keithamus

This comment was marked as off-topic.

@luwes
Copy link

luwes commented Mar 8, 2023

@keithamus I think @LeaVerou means to render those e.g <input slot="input" /> in the light DOM if they weren't placed there manually so they get styles from the document. We've been discussing this problem with Media Chrome as well.

I wouldn't recommend "sprouting" elements in the light DOM, no native elements do this as far as I know. And UI frameworks don't like this at all, a React hydrate pass for example will do a full client re-render if there is a DOM tree mismatch in elements. (attributes are okay I think, just gives a warning)

@keithamus
Copy link
Collaborator

Ah yes I understand now. I've hidden my comment as it was made from an incorrect basis of understanding.

@bathos
Copy link

bathos commented Mar 8, 2023

You don't want the page author to have to reach into their styling framework and add ::part(input) or ::part(button) for that.

The selector problem isn’t specific to custom elements, is it? A list of selectors including many pseudo-element selectors is needed for describing “all buttons” without any custom elements in play: ::-webkit-calendar-picker-indicator, ::-webkit-file-upload-button, ::file-selector-button, input[type=button], and input[type=submit] come to mind. I’d guess there are more than that, too?

I don’t think that’s ideal either, but I suspect a solution ought to address the problem generically rather than just for custom elements. I wonder if things would change here if native composite widgets all began exposing their buttons as ::part(button), say — generic style sheets could eventually end up “friendly” to custom elements with button parts as a consequence because it becomes a matter of echoing native-established part-naming conventions.

@LeaVerou
Copy link
Author

LeaVerou commented Mar 8, 2023

@bathos:

The selector problem isn’t specific to custom elements, is it?
[...]
I don’t think that’s ideal either, but I suspect a solution ought to address the problem generically rather than just for custom elements.

Literally, from my first post:

This also affects built-in components as well:

@bathos:

I wonder if things would change here if native composite widgets all began exposing their buttons as ::part(button), say — generic style sheets could eventually end up “friendly” to custom elements with button parts as a consequence because it becomes a matter of echoing native-established part-naming conventions.

It's not just about buttons and inputs, it's just that the most common use cases involve these. There are more examples in my first post.

@matthewp
Copy link

matthewp commented Mar 16, 2023

I find this thread fascinating because it gets into the heart of what the use-cases are for web components, and we can see that there are competing visions for what that is.

I tended to side with @caridy and @domenic's idea, of web components being decoration on top of light DOM where the interesting stuff was.

The current problem with this vision is that it only works if you are ok with the fallback. As an example:

<fancy-input>
  <input type="text">
</fancy-input>

This works as long as you are fine with regular <input>. But I think the thing we've seen over time is that people aren't ok with the default built-in elements. They find them lacking. And that's why they desire to build better ones.'

The fix for this problem would be, the built-in elements need to get better. Probably much better. If that happens then the fallback is not so bad, and so using web components for extra nice-to-have decoration starts making more sense.


The other competing vision of web components as essentially "macros" for more DOM has the problem that it's not a portable concept. Web components only run on the client-side, but many sites want/need their content to be included in initial HTML. So then you get into "server-side rendering" which is difficult for a number of reasons, but even if you accept the tradeoffs you are reducing the number of people who can use your components. It has to be those who are only using JavaScript on the backend, and only those who are using a certain framework for which your SSR solution works.

So this approach is possible, given you are willing to accept the tradeoffs, but it's no longer portable. And portability is one of the main reasons to want to use a standard.

@LeaVerou
Copy link
Author

The other competing vision of web components as essentially "macros" for more DOM has the problem that it's not a portable concept.

Sigh. Not sure if it's worth pointing out, for the Nth time, that the use cases that motivated this thread are not about the model of web components as macros. It's like people are so convinced that there can only be two views on what WC should be able to do, they desperately try to shoehorn any view into one of these. Every time I think I've finally explained the problem statement well (even with a concrete example here), someone else comes along that has misunderstood the entire problem statement, and I’m out of ideas about how else to explain it. 😔

I tended to side with @caridy and @domenic's idea, of web components being decoration on top of light DOM where the interesting stuff was. The current problem with this vision is that it only works if you are ok with the fallback.

I’m…honestly not sure how this relates to the problem statement. Is the idea that <fancy-input> should only ever use form elements provided by the user in the light DOM, and authors should simply not use form elements in the shadow DOM? What is the shadow DOM for in this view? Just divs and spans around light DOM content?
Thought exercise: What should the HTML API be for a dual handle slider component?

@matthewp
Copy link

@LeaVerou Categorization is a natural thing to do when breaking down a large problem space with many different solutions. If you think I've miscategorized the type of components you are talking about, then what category do you think they belong to?

From your example, <img-input> only works after its JavaScript has run. That means it can be used in certain contexts, a client-side rendered app that expects JavaScript to run for the page to be functional. But for many other types of sites where JavaScript is not a requirement, but a progressive enhancement, using <img-input> means that you are potentially sacrificing functionality (the ability to upload an image) if for some reason the JavaScript does not load (if the CDN is down, for example).

Is the idea that should only ever use form elements provided by the user in the light DOM, and authors should simply not use form elements in the shadow DOM? What is the shadow DOM for in this view? Just divs and spans around light DOM content?

The idea is that all important elements should be in the light DOM. That is, something you would consider a requirement for a functional page. If <fancy-input> is doing something you deem to be unimportant, and purely additive, then it would be ok to put it in the shadow DOM.

So, as an example, a submit button for a user to buy your product is very important, it should always be in the light DOM or you are risking a chance of not receiving payment. What is less important is up to the individual site. I tend to think everything is important, otherwise why is it on my site in the first place.

@DarkWiiPlayer
Copy link

Okay here's an attempt at breaking this down in a reasonable way:

All web components have two types of elements:

  1. Light-DOM elements, which constitute the "actual" content which is interesting to the user and
  2. Decorative elements that constitute the actual functionality of the component additionally to that of the elements in 1.

In the case of a light-dom input box, the "interesting" content is the text-input, which already gets styled from the outside. We can ignore this as it already works.

In the case of an image carousel, the interesting content are the images, provided by the HTML author, but the forward/backward control buttons are part of the additional functionality that the component provides. This is what we care about.

As has been shown above, putting these in the light-DOM is not optimal, because they're functionality that the component is supposed to provide.

This distinction into "user-provided" and "component-provided" functionality and elements seems (to me at least) universal to all the possible applications of web components, even if at a different split.

And I really hope that we can now continue to discuss the actual use-case, because if the discussion continues this way there won't ever be an actual resolution.

@bahrus
Copy link

bahrus commented Mar 16, 2023

I suppose one viable solution might be to have slots like the above, and imperatively generate content for the slots if they are empty. I guess this could work but it feels like a hack, and is fairly weird.

This actually touches on a pet peeve (or at least concern) I have, which I wonder if it could be rectified as part of this proposal?

First, even implementing this hack would be problematic without this issue being finally resolved. But even if that precondition is resolved, I agree that an element spawning its own light children seems a bit counter to how elements are supposed to behave (where even setting attributes is heavily discouraged).

Now it is possible to provide default slot content. I would have expected this to behave like an optional JavaScript parameter -- indistinguishable from having it passed in from the outside. But default slot content doesn't fire the slot changed event, and doesn't automatically inherit the styling one would get if the slot content is provided externally. It is possible to style it, but as far as I know we can't specify it to inherit styles from the parent Shadow DOM realm.

I would propose an attribute to make it behave like it was provided externally.

I can't think of a good attribute name for describing what I would have hoped is default behavior, but something like:

<slot name=file-selector id="upload-wrapper" treatdefaultcontentlikeitwaspassedin>
	<input type="file" accept="image/*">
	<button part="button browse-button">Browse…</button>
</slot>

This would cause all slotchanged events to fire and assignedNodes to be set without using flatten, and inheriting styles.

Not sure if this would also resolve all use cases @LeaVerou is thinking of, but maybe some?

@keithamus
Copy link
Collaborator

keithamus commented Apr 21, 2023

WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (#978 (comment)), heading entitled "Theming / open styling".

In the meeting, present members of WCCG reached a consensus to discuss further in breakout sessions. I'd like to call out that #1006 is the tracking issue for that breakout, in which this will likely be discussed further.

@luis-pato
Copy link

luis-pato commented Jan 18, 2024

I think ::part() is nice and works well, but for big / composed WCs it can be cumbersome to expose and document parts for every element inside a component.
So what if there would be something like a ::part-with-descendents which would allow us to expose an element of the WC including its children.

For instance:

<ul class="list" part-with-descendents="amazing-list">
    <li class="list__item">This in an item</li>
    <li class="list__item">This in another item <span class="list__banana">with something special</span></li>
</ul>

And then:

cool-component::part-with-descendents(amazing-list) {
    border: 1px solid red;
}

cool-component::part-with-descendents(amazing-list .list__item) {
    color: tomato;
}

cool-component::part-with-descendents(amazing-list .list__banana) {
    text-transform: uppercase;
}

This would allow the WC author to expose whole sections of the component, or maybe even the whole component if they want to.

@caridy
Copy link

caridy commented Jan 18, 2024

@luis-pato that will probably defeat the whole purpose of a controlled styling process, giving consumers of the web component the ability to modify the styles of any internal implementation details.

@luis-pato
Copy link

@caridy Maybe. But WC authors could expose only those parts they want the devs to be able to modify. Much like the ::part() selector, but also allowing for the children, if that is wanted...

@LeaVerou
Copy link
Author

Well, I completely forgot I had started this thread, and just commented the following in #909 :

I've been thinking about this problem on and off for years.

The problem with open-stylable

I think a big use case is having components that are able to adapt to the style of the page they’re dropped in (especially when combined with inherit()) without requiring author effort to integrate them. This means that anything that requires the surrounding page to use a special opt-in (e.g. /shadow/) is suboptimal (it's better than nothing, as it may be adopted as a convention, but it's still suboptimal).

There are use cases that truly don't need outside-in style encapsulation. Using WC by a page author for templating within that page is a prime example of that. It would also be perfect for something like <md-block>. An all-or-nothing solution like open-stylable will work well for such use cases.

However, for reusable widget use cases (typically not written by the page author, but a third party), usually some degree of style encapsulation is desirable, depending on how much you intend the element style to diverge from a typical instance of its type. E.g. suppose you’re creating a modal dialog component. You are far more likely to want to inherit button styles from the surrounding page for its footer buttons (e.g. OK, Cancel, etc.) than for the close button.

Use case patterns

Looking at the use cases under that pain point, I do see some patterns:

  1. Component styles should not leak out. That’s a type of style encapsulation that is almost always desirable.
  2. You typically want select page styles to trickle in, but you don’t typically want to pull in entire stylesheets. In my experience, you typically you want to pull in base styles, i.e. elements and their states (things like input[type="text"], input:not([type]), button:hover, button:focus:not(:focus-visible), select > option. For that reason, you also typically don’t need these page styles to match across Shadow DOM boundaries (not that there aren’t use cases for that, but I think they are very few)
  3. As discussed above, you need to be able to opt-in on a per-subtree basis, not for your entire Shadow DOM

Potential solution 1

I wonder if a solution that would fit these use cases better AND may actually be more implementable might be something like:

  • We introduce an attribute to control how style encapsulation works (name TBB, let’s call it adoptstyles for now). It inherits, and using adoptstyles on a descendant changes the behavior for that descendant subtree. Or, alternatively, each element needs to opt-in separately. Or, even better, it’s a CSS property so both patterns are easy.
  • It starts off with three values, let’s call them none (default), basic, all. none is the current situation and all is what open-stylable does, but for that particular subtree. basic only applies page rules whose selectors match certain criteria (discussed below). This also gives us room to introduce more keywords in the future.

Selector matching would happen entirely within the Shadow root.

Which rules should basic pull in?

This will likely need a fair bit of iteration, but I think a reasonable starting heuristic is to eliminate selectors containing any authorland identifier, for example selectors that include:

  • ids and classes
  • All attribute selectors where the attribute starts with data-
  • Attribute selectors where the attribute is class, id, or name

Potential solution 2

Same as above wrt the opt in, but instead of filtering out authorland selectors, the entire stylesheet is imported but as a different CSS layer, so that it’s trivial to override.

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

No branches or pull requests