-
Notifications
You must be signed in to change notification settings - Fork 657
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
[css-shadow-parts-1] Can class name selectors apply to a part? #3431
Comments
No, this isn't possible, by design. The point of ::part is to hide the internal details of the component, and expose only exactly the parts the component author explicitly wants to. Random classes are an internal detail; there's no telling what use the component author has for them, or whether they're appropriate for outside code to use. One can give a single element multiple part names, and differentiate state based on that. I also thought that you could do, say, @fergald , what do you think? |
@tabatkins Thanks for your thoughts! We care less about the precise mechanism and more about the scenario. My initial thought is that your multiple-parts solution would probably be sufficient to support styling our tabs component and a number of similar components. I do wonder if we'll hit cases where part state is more complex than a binary value, or multiple state values are in place. Another approach would give the author a way to expose only those part aspects that should be considered available for styling purposes. Using attributes as an example, something like
might explicitly open up the component-specific
We'll continue exploring the application of shadow parts to our library and look for cases were the current spec appears to be insufficient. |
@tabatkins Implementaiton-wise, supporting multiple part names ANDed together seems easy. Adding a separate |
@fergald I'd suggested attributes so that someone could target specific values for non-binary attributes via attribute selectors: e.g., I can certainly understand why multiple part names would be simpler. I'll do a preliminary review of our components looking for cases where that might not be sufficient. |
I think there would be ergonomic advantages to We also need to think about forwarding from deeper shadow-trees. Would we need a way to forward and rename attributes? If they're just part names we already have that ability (although we would want wildcard forwarding to be able to forward |
I've done an initial review of our Elix component library to look for situations where someone might want to style a part based on component state or part state. These components, roughly 60 in number, are intentionally generic in appearance so that they can eventually be styled to match an app's visual aesthetic or branding, so they seem like a reasonable corpus of use cases for shadow parts. Initial findings
So out of our 60 components, it looks like we'll hit this issue in a substantial portion of them. I think allowing multiple part names will cover the scenarios I can envision so far. That said, for the 21 cases described above where state is essentially an enum, it'll get cumbersome to define part names for each enum value. In the Part attributesAs I think through using multiple part names to indicate part state, I'm becoming worried that it'll get quite complex. The problem is that a The reason I suggested considering something involving attributes is that native HTML elements already reflect their inner state as attributes for styling purposes. It'd be reasonable to ask component authors to do the same for those properties that might be implicated in styling. We can easily extend our
My suggestion was not to let someone set attributes inside Example: Suppose a <menu-button part="menu-button" part-attributes="horizontal-align opened"> Or, if they only want to let someone style the "opened" attribute on the part, they write The menu-button[horizontal-align="left"] { ... } And someone targeting a my-component::part(menu-button)[horizontal-align="left"] { ... } That is, it's essentially the same syntax.
FWIW, I believe that, if the person exposing a part deems it reasonable to expose an attribute on that part for styling, that other component authors further up the chain would probably be fine leaving such flexibility in place as they forward the part. It's zero additional work for them. That is, I don't think someone forwarding a part will need to narrow the set of attributes available for styling. Apologies if the examples are abstruse. The number 1 request we get from potential users of our library is whether they can style the components in CSS, so we're interested in making this easy for those users. If it'd be helpful to talk through any of this, I'll make myself available for a chat. |
Thanks for all that info. The All that said, perfect is the enemy of good. The value of being able to do this at all seems high and it also seems unlikely that we would ever go as far as adding something to allow transforming attribute names and values to allow a stable API to be presented in cases like these.
I'm not sure I follow this example. The containing component would not put a
If we added wildcard exports then this would change to be
only the inner component would have to worry about tracking state and munging things to look like It's still hacky. I think |
I always thought that this will be possible only via CSS tokens/states (WICG/webcomponents#738), where the possible states of your component are part of the public API of it, and using them in conjunction with parts seems to be enough for such styling, e.g.: my-tab-component::part(tab-button) {
color: black;
}
my-tab-component:state(selected)::part(tab-button) {
color: red;
} @tabatkins @domenic can you confirm that this compounding will be possible? @JanMiksovsky this, of course, is not equivalent to what you're asking because it is not really the state of the part, but the state of the component that exposes the part, but I think it will cover a lot of ground. Also, consider that we haven't really work on the css state proposal, we need a formal proposal first. |
@caridy Yes, this would be helped by representing component state via custom pseudo-classes. I referenced those in the original post, but I wasn't sure whether those were on a track to happen sooner than basic ::part support. Note also that I'm talking about letting parts, not just overall components, that have state. In the tabs example, the overall tabs doesn't have the selected state, the individual tab buttons have a selected state. In other words, rather than something like: my-tab-component:state(selected)::part(tab-button) {} I'm more concerned about supporting something like my-tab-component::part(tab-button):state(selected) {} where the
@fergald You're correct, thank you for catching that. I thought it might also be interesting to share some further experimentation here. We've created a working styled variation of our Elix Tabs component that uses CSS parts.
The second demo is the same tabs component as the first, styled using CSS parts. (The only exception is the crossfade effect on the pages, which we achieve by other means not relevant to this discussion.) We achieved this style tabs demo through a riff on @tabatkins's idea to use multiple parts. Here, we apply one part name ("proxy") to unselected tabs, and a different part name ("proxy-selected") to the selected tab. (Apologies for the ambiguous "proxy" term instead of "tab button" in the demo CSS, but the base class for our Tabs component is actually an extremely general component called Explorer that is used for much more than just tabs.) Using different part names like that is obviously cumbersome, but works today, and gives us a glimpse of what we'd like to let our component customers achieve through CSS parts. Although there's still a long road ahead, we're quite excited by being able to do this at all. The stock Tabs (elix-tabs) element looks mundane but provides a large number of features that may not be obvious: keyboard support (via arrow keys while a tab button is selected), ARIA, resilience if tabs are added/removed at runtime, etc. It will be fantastic if someone can receive all those benefits and still get the visual aesthetic they want through CSS. That said, we remain convinced that some form of per-part styling will be necessary to let customers style our components as they would like. That could take the form of:
|
@JanMiksovsky I don't think |
@caridy I think that's unavoidable — and also fine. I'm starting from the assumptions that:
So if I build components from subcomponents, then I also take on both the risk and opportunity of being able to swap out those subcomponents for better ones in the future. That might break someone's careful styling of my component that assumes the old subcomponent. That's unfortunate, but to me no different than the possibility that future work might force me to make a breaking change in a component's JS API. And the very existence of a I would think devs could approach CSS support exactly like JS support: try hard not break contracts, but allow for that possibility, and use conventions like semver and documentation to signal when contracts are getting broken. |
JS also has a lot of tools to help authors hide things (closure-based state, wrapper objects that expose a more minimal API, etc). HTML and CSS don't; they're fundamentally simpler languages in this regard. We still don't want to expose anything from the component that the component author isn't explicitly deciding to expose (or at least, explicitly wildcarding, with the knowledge that what they're doing is a little dangerous). Implicitly exposing extra data from subelements breaks encapsulation in an important way here. |
@JanMiksovsky thank you for your detailed post at #3431 (comment) , and apologies for taking a while to process it. I think I'm not fully understanding something though, which is why in your component libraries there are so many parts-with-states. If you look at built-in HTML elements with UA shadow DOM (or other complex internals) today, they all use shadow DOM for encapsulation. The encapsulation is complete enough that the parts themselves don't really have any state; instead that gets bubbled up to the host element. Any case where there's a complicated-enough sub-element that has its own potential states ends up being done through light DOM, not shadow DOM. So for example, you have a my-tabs-component, but from what I can gather in the OP, you have put all interesting sub-parts inside the shadow DOM. This means you have to re-expose them as But this isn't how we would design a built-in element. There, the interesting sub-parts would be in the light DOM, e.g. something like <tabs>
<tab>
<tabbutton>One</tabbutton>
Page One
</tab>
<tab>
<tabbutton>Two</tabbutton>
Page Two
</tab>
<tab>
<tabbutton>Three</tabbutton>
Page Three
</tab>
</tabs> Indeed, I see you even have an example of that later down in your own docs, with "Custom content in default tab buttons"! In other words, parts make sense for things that are internal parts of your main component, like In general one rule of thumb I might come up with is that any time there can be an arbitrary number of the sub-control, it's probably first-class enough to need light DOM exposure. If the sub-control is 1:1 with the main custom element, then maybe it's just a part. With this framing in mind, do you have any other examples from your 26+21+6 components where it's a more clear-cut case of the sub-part having its own state, that run into the issue described in the OP? Because the specific OP issue doesn't make sense to me anymore now that I look at this in more detail. Especially seeing how you explicitly encourage users in your docs to have light DOM |
@domenic I agree that for many leafy "widgets" along the lines that the UA ships, composition via light DOM is preferable for the reasons you list. There are probably still interesting cases to investigate that still expose this issue -- perhaps the "carousel" example is a good one to study: <my-carousel>
<img src="a.png">
<img src="b.png">
<img src="c.png">
</my-carousel> In its shadow DOM, it might create a "dot" affordance in the shadow DOM for each light DOM item in the carousel, and swiping images changes the selected dot. I could see wanting to allow style |
However (and I think this is where @JanMiksovsky's "Devs want to build components from smaller subcomponents" comment comes in), if you're using Web Components as primitives not to just build leaf widgets, but to also compose them into higher levels of reusable functionality, you can pretty quickly hit these types of use cases. Consider a mortgage calculator widget that is composed out of several custom elements (e.g. sliders and buttons and such), and some of those could have stylable state. It would not make sense to require the user to supply the "interest rate slider" in the light dom, and yet it might very well have styleable state. These are cases that the UA does not hit because it is not in the business of vending such high-level components (but it would be a shame for Web Components to not be able to support these cases). |
Well, sure, but if you're composing them, you don't need to hide all the pieces you're composing in the shadow DOM. If you want to give your user fine-grained control over, and exposure to, the sub-components you're composed of, then you should expose them through the light DOM. I'd rather we not re-invent the light DOM by taking shadow DOM, and adding more re-exposure features until we've created just a duplicate, awkward-to-use light DOM. |
@domenic Thanks for giving this some thought; we really appreciate it. Let me offer some more use cases where someone will want to expose multiple sub-elements as parts, and where those parts will often have state that people will want to reflect in styles. Components with generated elementsThe carousels @kevinpschaaf mentions above are a common example of a category of components that want to generate subelements. Our stock Carousel does this, and we have others that generate page numbers, thumbnails, or labels drawn from image captions. Other examples:
In these types of components, it’s simply not possible to ask the component user to generate the elements themselves and place them in the light DOM. The generation of that content is much of the component’s value. A component could try to generate its own light DOM content, but that gets messy, no?
Components with stock contentAnother category of components with parts with multiple instances are components that provide stock content such as tools. As a simple case of such stock content, the carousel above has left and right arrow buttons. An author will want to style the general appearance of those buttons, along with :hover and :disabled states. Although there are just two of these buttons, conceptually it’d be nice to define such styling once, rather than having to style As the number of commands increases, the burden of having to style each button as its own independent part increases. A rich text editor or image editor component which offer the user a toolbar with a number of buttons for editing commands. Those buttons are stock content — elements built into the editor. The creator of the editor component wants to let someone style those buttons as a collection, instead of having to style each command separately. The buttons have state: disabled, checked, etc. It should not be necessary to expose those commands in the light DOM to make them available for styling. A component that wants to provide elements for the author — generated elements like those dots, or stock elements like those editing commands — is going to want to let the page author style those elements. In both cases, it’s cumbersome to try to do that through light DOM elements. As soon as we ask the component user to put something in the light DOM that could in any way be inferred or generated from their real data/content, some clever person will create a script to automate the creation of those light DOM elements. They’ll share that script with their team as a more helpful wrapper for the actual component. Different team members will create different wrapper scripts for different components; the wrappers will behave slightly differently. If that happens, it would mitigate many of the advantages of having introduced a component model. I don’t think we’re trying to give the user complete fine-grained control over the subelements. The user can’t modify those subelements directly, they can’t listen to events on them, etc. The interest is specifically on styling. I share your concerns about increasing complexity, but think focusing on the styling problem prevents full-blown replication of a duplicate light DOM. |
Very interesting. It seems like we should probably be investing in more idiomatic solutions to those problems, instead of trying to shoehorn them into the web components model. For example, if you really want a component that's not an encapsulated component, but instead a "macro" for generating large chunks of light DOM, we should consider what is an ergonomic way to introduce DOM macros into the platform. Then, we won't have this weird thing where you're using shadow DOM for something that isn't really meant to be encapsulated, and then needing to work around it by re-exposing everything with these pseudo-light-DOM technologies. I don't understand the difference between generated and stock content from this perspective; they both seem to be the case of "the component author wants to generate some sub-elements for the component user without them having to manually include them".
Indeed, it seems like the solution space should be to try to standardize or give guidance for these kind of generation scripts. One example primitive that might fit in well here would be "HTML includes". |
:grumbles in declarative: ^_^ But yeah, agree all round here. There are use-cases for exposing states on shadows, but they should be handleable just by letting you specify several part names; getting more complicated than that means you're probably trying to use Shadow DOM to solve a different problem, and we should just solve that problem directly. |
Aside: this is a really nice issue thread of people providing comprehensively documented, politely argued perspectives on a complex issue. It was very easy to catch up on the issue. Yay, you! I agree that state/pseudoclass selectors are necessary for fully theme-able custom elements. The web component author should be able to define what states exist in the component that justify a different rendering, and the web page author should be able to adjust the theme accordingly. And for a web component that contains theme-able parts, it is only natural that those parts might have states distinct from the state of the rest of the component. While @domenic's general rules for composability are good guidance, I think @JanMiksovsky's examples of complex widgets or data display are all equally valid. I strongly disagree with the most recent comments by Tab and Domenic, that these complex widgets are a failure of encapsulation. Encapsulation means that the details of implementation of lower levels should be abstracted away at the top level. It doesn't mean that the lower levels can't be complex. It seems totally reasonable to me to have a component where the top-level light DOM defines the unique content, while the shadow DOMs generate complex widgets for modifying that content (like a rich text editor) or displaying it (like an interactive data visualization). From a script point of you, the outer web page only cares that the inner widget is doing its job, maybe watching for updates to the light-DOM content. But for styling? To create a full theme for a rich text editor, you're going to need toggle button states and focus states and selected text states and checked item in a drop-down menu states. So that means states not only for the widget as a whole (e.g., empty, saved, invalid), but for individual repeated parts like buttons and drop-downs. I do not like the idea of describing "a part that has a certain state" with a combination of multiple However, I also do not like the idea of using regular CSS attribute selectors to snoop into the shadow DOM. We've gone down that route before, and it contradicts with the goals of encapsulation. So, I agree that a my-widget:state(active)::part(slider) {
/* themes for the slider when the widget is in the active state */
}
my-widget::part(slider):state(active) {
/* themes for the active slider in the widget */
} Possible
|
Belated brain flash: What if we—hear me out, now—used CSS syntax? So states for the custom element could be defined with an @state { /* state for the entire widget */
checked: #checkbox:checked ;
/* the widget is checked if the shadow element with id `checkbox` is checked */
}
@state drop-down-button { /* state for any element with part="drop-down-button" */
focus: :focus-within;
/* the part will match the focus pseudoclass for the outside tree
if it contains a focused element within the shadow tree */
} If allowing colons inside a value in a declaration-like structure is a syntax issue, the selectors could be wrapped in |
One potential problem with this is that custom elements already match certain state-like pseudoclasses. The author-defined states would need to cascade on to the default ones. For a generic custom element ( @state {
hover: :root:hover;
active: :root:active;
focus-within: :root:focus-within;
} If an author provided a new definition of the For custom elements that upgrade native elements with the For example, consider an enhanced version of |
On the contrary to what you've stated, reading DOM state to update We should go with a JS DOM API. It would be simpler & more efficiently implementable. |
Understood. But updating that DOM state from JavaScript would slow things down for interactive pseudoclasses that change a lot. (Like the example in the webcomponents issue for re-implementing |
Hardly. What would be way more expensive is the updating of the relevant style, layout, & paint. What's really expensive is having to track the dependency between elements, and figure out if hovering an element can in turn affect other element's Note that we DO need JS-based solution regardless because elements can have states other than the ones natively supported by the browser. e.g. custom checkbox implementation may supported, in addition to Once we accepted that we do need a mechanism for JS to set a custom state on an element, then it's only natural to let the script update all the states instead of inventing separate / independent mechanism to do the same. Otherwise, we'd have to figure out how selector based approach & JS API interact, etc... |
I was assuming that fully custom states would be set the old-fashioned way, by setting a class or data attribute from JS. But yes, that would clearly be less efficient than just adding a token to a And you clearly know more than I do about implementation headaches, @rniwa. I was thinking of this purely from the perspective of passing values up the tree of shadow trees, but selectors are maybe a little too generic for that. I'd forgotten that things like The first half of my comment still stands: So if there isn't a declarative way to do that, there needs to be a I also still like the idea of letting the custom state mechanism override standard pseudoclasses ( |
In theory, that sounds like a valid use case but I really don't see how we can implement such a feature without having some catastrophic performance characteristics regardless of whether we do it in CSS or JS. |
JS set |
I don't see how. The fundamental problem is states like |
Affecting random other elements how? I'm failing to see how Amelia's proposal leads to O(n^2) behavior, but if you elaborate perhaps we could find a solution, possibly via a set of limitations that fixes the performance characteristics without crippling the use cases too much. |
We're experimenting with the shadow part support in Chrome (behind the flag), and have a number of situations where we'd like to conditionally style parts based on the state of the relevant part.
E.g., we have a Tabs component, and would like to expose the tab buttons as parts for styling. Significantly, the selected state of the tab button is relevant to styling, i.e., a selected tab button should look different than an unselected tab button.
The selected state of the tab button is not reflected in any existing pseudo-class. (While it'd be theoretically possible to reimplement the tab buttons as check boxes to expose the
:checked
pseudo-class, that's an extremely awkward approach just to get some styling.)Short of having some way for components to expose custom pseudo-classes, we were wondering if it'd be possible to access classes on a part: e.g.,
Here the component author communicates part state to the outside world by applying CSS classes to the parts.
It's not explicitly indicated in https://drafts.csswg.org/css-shadow-parts/#part, but our experiments in Chrome at least suggest that the above is not supported. That places hard limits on how customizable we can make our components, even when
::part
is widely supported.It doesn't seem like supporting class selectors on a part leaks much critical information or would produce brittle components. If the component author needs to refactor their component's shadow tree — e.g., to replace one type of shadow element with a different type of element — it'd be safe for them to support the same class names on the new element.
The text was updated successfully, but these errors were encountered: