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

[css-shadow-parts] Unifying ::part() and ::--foo #4900

Open
tabatkins opened this issue Mar 24, 2020 · 28 comments
Open

[css-shadow-parts] Unifying ::part() and ::--foo #4900

tabatkins opened this issue Mar 24, 2020 · 28 comments

Comments

@tabatkins
Copy link
Member

tldr: We should allow part names to also be selected as full custom pseudo-elements names, a la my-component::--heading in addition to my-component::part(heading).


In the Web Components virtual f2f today, we discussed custom pseudo-classes. In the thread about :state(foo) vs :--foo syntax, Maciej brought up the reasonable point that switching away from :state() to :--foo, but leaving parts as ::part(), is inconsistent.

I explained the reason behind maintaining ::part(): due to CSS's bad syntax choices in the past, pseudo-element syntax implicitly contains a combinator, moving the subject of the selector into the element's "pseudo children"; as a result, you can't stack pseudo-elements to apply multiple conditions to a single pseudo-element. However, we want to allow that for parts - we want a calendar widget to be able to expose day elements for styling with something like part="day weekend saturday week-1 us-holiday", and let stylesheets select on a combination of those qualities, like ::part(day weekend us-holiday)

However, Maciej's point is still valid! Also, people have wanted ::--foo syntax, to mirror normal pseudo-elements, and things like UA pseudo-elements (::-webkit-scrollbar-thumb, for example).

Jan's idea was to unify these - allow parts to be selected either with ::part(foo) or with ::--foo; if you only need to select on one property, you can use the more direct form, but ::part() remains if you have more complex selecting needs. This received general approval in the call, and I think it's a good idea personally. General thoughts?


I think there's one important detail to decide, which is how to handle existing part='' content. In general, CSSWG has made the decision that dashed-idents should be used consistently thruout APIs: you reference the --foo custom property with var(--foo), not var(foo), etc. In other words, the -- is part of the name, not a context-specific indicator of custom-ness.

Currently, tho, part='' allows any ident, not just dashed-idents. How should we deal with this? I see a few options:

  1. (I don't like this, it's bad, but for completeness:) part is unchanged. If you define part="foo", you can reference it as ::part(foo) or ::--foo. In other words, the -- is a context-specific indicator of custom-ness, but is not used when defining it, or referencing in ::part(). (Note: if you said part="--foo", it would be referenceable as ::part(--foo)or::----foo`.)

    This is backwards-compatible with existing code, but breaks consistency with how dashed-ident features are used everywhere else in CSS, both currently and planned. The component author doesn't make a choice on how to expose things, but the component user is presented with two fairly different names to refer to.

  2. part is unchanged. If you define a part as a dashed-ident, it can be referred to as a top-level pseudo-element. That is, given part="foo --bar", you can use ::part(foo), ::part(--bar), or ::--bar validly. ::--foo will not match, nor will ::foo.

    This is backwards-compatible with existing code, and consistent with CSS design principles for dashed-idents. However, it imposes a choice on component authors of whether they want to expose parts as top-level pseudos or only within ::part(). (There might be a good reason to make this distinction? Unsure, but I suspect not.)

  3. We restrict part to only allow dashed-idents. part="foo --bar" only defines one part name, --bar, which can be referenced as either ::part(--bar) or ::--bar. (The "foo" is ignored, as part of our forward-compatible error handling that silently drops things that aren't understood.)

    This is not backwards-compatible; virtually all existing content would become invalid and stop defining parts. However, it means there's no choice on the component author's side, and the component user has a closer relationship in the two syntaxes.

I slightly lean toward 3, but I'm unsure how much back-compat pain we want to take on. I'm fine with 2 if it's preferred by others. I'll fight against 1, it's terrible.

@plinss
Copy link
Member

plinss commented Mar 24, 2020

I'm in favor of this change and also favor 3. I was considering something along these lines as a migration from the current syntax to ::--foo. Hopefully the :part() syntax can eventually be deprecated.

I still have issues with the current design of shadow parts using multiple part names as an ersatz class/pseudo-class mechanism. I'd much rather see us fix selectors, e.g. your example could be ::--day:--weekend:--us-holiday (using custom state) or possibly something like ::--day(weekend us-holiday), but in that case 'weekend' and 'us-holiday' should probably not be part names but an additional property of the part.

I don't have an issue with parts having multiple names, but that should only mean it can be selected as either of those names, e.g. <custom-day part="--day --weekend"> matches both ::--day and ::--weekend.

@JanMiksovsky
Copy link

Credit where credit's due: I think the idea to unify these was from @justinfagnani.

@tabatkins
Copy link
Member Author

Hopefully the :part() syntax can eventually be deprecated.
[...]
I'd much rather see us fix selectors, e.g. your example could be ::--day:--weekend:--us-holiday (using custom state)

I disagree.

First, a more abstract objection: I don't think the tagname/classname distinction we currently have in the DOM is a useful distinction. It's something we inherited from DOM, but there's no meaningful difference between the two semantically; we even invented two "pretend I don't have a tagname" elements (div and span) to allow authors to supply their own semantic fully thru classes. The only thing tagnames meaningfully do is trigger browser-default behavior (a acts like a link, etc), but as far as CSS is concerned that's more or less a non-issue, and even then HTML is littered with exceptions (like all the various inputs clustered under a single input name despite varying behavior and semantics).

So I don't think we should be trying to introduce the same distinction for custom elements. Elements have a set of idents with class-like semantics; there's no need for a tagname-like semantic, and forcing authors to decide which of their several possible idents qualify as "most tagname-like" is a decision we shouldn't force authors to make. We want to offer a tagname-like syntax for convenience, nothing more.

Second, a more concrete objection: having a custom element expose its part semantics as pseudo-classes clashes with the existing custom pseudo-class proposal.

As I discussed in #7, I've come to believe that custom elements are indeed the most appropriate place in the DOM to expose custom pseudo-classes; in particular, I think there is no need to add them to arbitrary elements in the light dom. Custom Elements have their own, component-controlled semantics that need to be deployed as a separate namespace from the author-level semantic (classnames), so a pseudo-class with a distinct syntax space works out fine.

Exposed parts hit the same conflict: the part itself might be a custom element, which exposes its own custom pseudo-classes. The outer component already has a place to indicate class-like semantics on its elements, in the form of the part name; merging the two means there can be clashes between two not-necessarily-cooperating elements, same as a custom element exposing its states by manipulating its host's class attribute.

(In this, I am assuming that the part's custom pseudo-classes will be available for matching, same as most UA-defined pseudo-classes are. I don't think this is an encapsulation break.)

@tabatkins
Copy link
Member Author

So I gave some more serious thought to what it would look like to let parts expose pseudo-classes as well, and yes, I'm pretty certain it introduces unresolveable problems.

So here's my sketch of how this all would work (forgive me for being somewhat stream-of-consciousness with it, tldr at the end):

First, a component can, itself, expose custom pseudo-classes declaring its state to the consumer of the component. This uses the API we're already talking about, no elaboration needed here.

Then, the component can expose parts of itself as pseudo-elements. Since our current concept of "part names" are class-like, but the pseudo-element syntax is restricted to not allow that semantic, we change things up a bit - custom elements can just expose one or more ::part pseudo-elements, and can add pseudo-classes to give more detail. So a calendar widget's days would all be ::part:--day, and could be further selected with ::part:--day:--weekend:--holiday for specific styling.

(If we allow the author to denote one of the keywords as the "name" of the pseudo, so the user could instead write ::--day:--weekend:--holiday, this doesn't materially change my analysis, so without loss of generality, let's continue to assume this doesn't happen and you only get ::part.)

But the part could itself be a custom element, which can expose both (a) its own pseudo-classes, and (b) its own parts.

If the inner component's pseudo-classes are automatically exposed to users of the outer components, we have an obvious potential for clashes, causing upgrade hazards we've tried hard to avoid; the <x-day> component might expose a :--holiday pseudo-class of its own, in a way that clashes with the calendar's desired usage. (For example, it might assume US holidays, while the calendar is internationalized and can do different countries.)

If the inner component's pseudo-classes are not automatically exposed, but rather have to be forwarded by the outer component, this is avoided, but it means more work for the component author: you can't just use a <cool-checkbox> component as a part and be done; instead you have to manually forward all the form-relevant pseudo-classes the element might expose. We don't want to allow wildcard forwarding, for the same reason we don't allow that for part names without a guarding prefix: it reintroduces the possibility of clashes.

But that's not all, the inner component can also have parts to expose, which, per our existing resolutions, need to be explicitly forwarded as well. So the outer component has to have a separate mechanism to filter/forward the parts. But now we have an interesting question: how? By the pseudo-classes it exposes, presumably? Or does it distinguish between the (external) part-defined pseudo-classes and the (internal) component-defined ones, and only filter on the former?

And then how do you decide which of the forwarded part's pseudo-classes should be exposed? Filtering is necessary at every level, after all; there's no good reason you should be able to filter your own parts but not any forwarded parts. Do we need a third forwarding mechanism to handle those? How, exactly, do we identify which forwarded parts are having their pseudoclasses filtered? The only way to refer to them is by their pseudo-classes, after all; this isn't like our own parts, where the element being filtered is the one we are adding the attributes to. So instead we're now dealing with filtering sets of forwarded parts, and have to deal with single parts showing up in multiple such sets and having clashing forwarding advice, too.

(Luckily we don't recurse further; any components nested further within the inner component have already been forwarded and filtered by the inner component, and we just have to handle its results.)


So yeah, ultimately the issue is that we're collapsing levels, something which is notoriously fraught. Part names and component pseudo-classes are distinct namespaces because they're controlled by different authors, but if we push them into the same namespace, avoiding clashes becomes a lot more complicated, to the point that I don't think authors can reasonably navigate it.

So I'm pretty strongly still of the opinion that part names and custom pseudo-classes, despite their partial overlap in semantics, should be kept separate. And that custom pseudo-classes should be exposed on parts by default, without any filtering mechanism.

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Mar 25, 2020

(@tabatkins #4900 (comment))

As I discussed in #7, I've come to believe that custom elements are indeed the most appropriate place in the DOM to expose custom pseudo-classes; in particular, I think there is no need to add them to arbitrary elements in the light dom. Custom Elements have their own, component-controlled semantics that need to be deployed as a separate namespace from the author-level semantic (classnames), so a pseudo-class with a distinct syntax space works out fine.

I think you mean WICG/custom-state-pseudo-class#7.

@othermaciej
Copy link
Member

Now that CSS WG has resolved to rename :state(foo) to :--foo, please make this corresponding rename sooner rather than later.

It looks like the conversation wandered to many tangentially related things, but the crux of this issue is renaming ::part(foo) to ::--foo for consistency with the :state change. It's weird for one of these features to use a name and the other to use a -- prefix, since they are meant to be used in tandem.

@justinfagnani
Copy link

The conversation has some critical points though: This change is not a simple rename. There is no custom-pseudo-element equivalent to ::part(foo bar) or ::part(foo:--bar). I definitely have written elements with multiple part names that are intended to be used in combination like ::part(foo bar). We need to keep ::part() to preserve those use cases. Adding ::--foo for simple cases is fine.

@othermaciej
Copy link
Member

If that’s the case, it’s sad that consistency is not possible, and that impossibility of consistency was not really considered when resolving to do the :state rename.

@tabatkins
Copy link
Member Author

@othermaciej We did consider that. The initial post in this issue is the proposal as it stands - you should be able to match a part name like "--foo" as ::--foo. We need to preserve ::part() as well for the specific purpose of matching an element based on multiple names at once, which is otherwise impossible (as an unfortunate side-effect of how pseudo-element syntax works).

There's an open design question in the OP; opinions would be appreciated!

@othermaciej
Copy link
Member

Having two names for the same thing is kind of lame. I wonder if ::--(foo bar) would work for the complex cases?

@web-padawan
Copy link

As a library maintainer, I find the new syntax questionable in terms of developer experience.

::part(form-field):state(pristine)

👍 human friendly
👍 self-explanatory
👎 more verbose

::--form-field:--pristine

👎 confusing
👍 somewhat BEM-ish
👍 fewer bytes

Off-topic: what if we make it even more BEM-ish? 🤔

::__form-field:--pristine

@tabatkins
Copy link
Member Author

I wonder if ::--(foo bar) would work for the complex cases?

Hmm, it would work parsing-wise — that's a --() function, which is allowed by Syntax.

It looks a little clunky to me, but that might be a kneejerk. I suppose I'm not opposed to it. Is that something you want to champion, then?


@web-padawan: Note how, if these were built-in pseudos, you'd spell them ::form-field:pristine. Doing ::--form-field:--pristine keeps us as close to built-ins as possible without polluting the built-in namespace.

@othermaciej
Copy link
Member

Is that something you want to champion, then?

I don't know. It looks weird to me, but all the colon/dash prefixed things look weird to me, so that doesn't mean much. I do think it would be nice if we could avoid two aliases for the same thing.

Side note: is it useful to do what ::part(foo bar:baz) does for custom parts for built-in pseudo-elements? I could totally imagine a native date picking exposing parts similar to what you had for your custom calendar, but it would apparently have no way to solve the same problem. If so, perhaps what's really missing is a generic CSS syntax for pseudo-element multi matching?

@othermaciej
Copy link
Member

(Alternately, maybe what's needed there is parts with states. Then, in that example, day would be a part, but weekend, week-1, us-holiday and saturday would be states? Seems kind of right for us-holiday but kind of wrong for saturday.)

@tabatkins
Copy link
Member Author

tabatkins commented May 8, 2020

(Alternately, maybe what's needed there is parts with states. Then, in that example, day would be a part, but weekend, week-1, us-holiday and saturday would be states? Seems kind of right for us-holiday but kind of wrong for saturday.)

Right. ^_^ There's the issue of forcing the author to make a fairly arbitrary choice between a particular name being "tagname-like" or "class-like" for a given part, which I don't think is usually a very meaningful distinction.

There's also the issue of syntax - if "states" here are expressed as custom pseudo-classes, then we run into the syntax collision I outline in #4900 (comment), where two non-cooperating parties (the outer component author and the inner component author) are both trying to expose custom pseudo-classes on the same element.

And if they're not expressed as custom pseudo-classes, then how? Either we scope the names into a pseudo-class function, so you have ::--day:state(weekend) in which case you've reinvented ::part() but more verbosely ^_^, or we open up syntax a bit more and allow something like ::--day.weekend, so that the state-like part names are just classes.

I might be okay with that last option, btw! It still has the "tagname vs class" artificial distinction that I don't like, but I could live with it if others are happier.

(Alternately, we just use ::part, no parens, to name all the parts, and all the part names are classes, like ::part.day.weekend. I'd be happier with this variant.)

Side note: is it useful to do what ::part(foo bar:baz) does for custom parts for built-in pseudo-elements? I could totally imagine a native date picking exposing parts similar to what you had for your custom calendar, but it would apparently have no way to solve the same problem. If so, perhaps what's really missing is a generic CSS syntax for pseudo-element multi matching?

Yes, it probably would be useful. And luckily, if we go with option 3 from my original post (author-defined part names must start with --), then we could just reuse the same syntax! UA-defined parts would just be non-dashed: you could say input[type=date]::week1 and input[type-date]::weekend, or input[type=date]::part(week1 weekend) to select on both criteria.

(...I guess that means ::part(before) would be another way to spell ::before? I'd be okay with that, I think. "before" parts would only ever have the one name, so writing it with ::part() wouldn't add anything, but it would give us a consistent syntax story across the board.)

@plinss
Copy link
Member

plinss commented May 8, 2020

(Alternately, we just use ::part, no parens, to name all the parts, and all the part names are classes, like ::part.day.weekend. I'd be happier with this variant.)

This is actually very close to what I've been thinking lately.

I'm not happy with the conflation of tag-like and class-like behavior in part names. I strongly feel that shadow parts should really just be custom pseudo-elements and have a single tag-like name, just like built-in pseudo elements do. I want to expose built-in behavior not create something special and different.

I do see the value in the additional class-like behavior, and my current thinking is that could be exposed as classes of the pseudo-element to selectors. There's no way for anything else to assign classes to pseudo-elements so why not? Note that actual classes assigned to a part should not break the encapsulation of the custom element and be visible outside it.

e.g.

<custom-calendar>
    <shadow-root>
      <custom-day part='day' part-class='weekend holiday' class='internal-class'>1</custom-day>
    </shadow-root>
  </custom-calendar>

(I don't care what the 'part-class' attribute is really called)

A weekend holiday would be selected via:
custom-calendar::--day.holiday.weekend { ... }

The 'internal-class' can be used by stylesheets attached to the <custom-calendar> but is not exposed to anything that contains a <custom-calendar>. If the <custom-day> itself exposes custom state or additional parts, those would simply be exposed and could be selected like:
custom-calendar::--day.holiday.weekend:--active::--day-part { ... }

I'd be ok with parts having multiple part names, but those would be an 'or', e.g. you could select <custom-day part='foo bar'> as either ::--foo or ::--bar, but never ::--foo::--bar because that would be a bar part inside the foo part. (Which also brings the question if the part name should be specified as part='--foo', I think so). Multiple tag-like part names would allow flexibility to evolve the internals of a custom element without necessarily breaking users, but then can't be (ab)used for class-like behavior.

Alternatively we could define the first part name to be the tag-like name and others to be class-like, but I think that's problematic when you start manipulating them, e.g. toggle the first one off and you just redefined the tag-like name.

If the class syntax doesn't work for some reason, my fallback would be exposing the part's class-like names as param of the pseudo-element, e.g.
custom-calendar::--day(holiday weekend) { ... }

Either approach gets rid of ::part and aligns this with custom pseudo-classes.

@tabatkins
Copy link
Member Author

I strongly feel that shadow parts should really just be custom pseudo-elements and have a single tag-like name, just like built-in pseudo elements do. I want to expose built-in behavior not create something special and different.

Note that that's the motivation behind my proposal in the OP; preserve the more useful behavior of class-like names, but also preserve the looks-like-builtin nature of pseudo-element names.

If the itself exposes custom state or additional parts, those would simply be exposed and could be selected like:

Recall that we (intentionally) don't allow ::part() nesting - if you want to expose a part of your part, you "forward" their part into your part-map. This hides details of the internal structure of the element.

(It also avoids the oddity of a sub-component exposing nested parts, but <parent part=foo><child part=bar></child></parent> exposing "foo" and "bar" as siblings, not nested. And we don't want to get into the hornet's nest of distinguishing "real" children from sub-components part "children"...)

@plinss
Copy link
Member

plinss commented May 8, 2020

Note that that's the motivation behind my proposal in the OP; preserve the more useful behavior of class-like names, but also preserve the looks-like-builtin nature of pseudo-element names.

But by using ::part you're shunting all shadow parts into being a certain type of pseudo-element, rather than allowing custom elements to create their own type of custom pseudo-elements.

Again, I agree that class-like behavior on shadow parts is very useful, but we can expose that as classes of pseudo-elements rather than inventing something new and different.

Another thing that occurred to me, I think it was Amelia who suggested that since custom state now uses a '--' prefix, we can potentially allow the API to set a safe-listed set of built-in pseudo-class names in the future, e.g. 'active', 'checked', etc. The same applies to custom pseudo-elements, by putting all the custom pseudo-elements behind a '--' prefix, in principle we can allow certain built-in pseudo-elements as well, e.g. allow a custom element to have a 'placeholder' or 'marker' shodow part that can be selected via ::placeholder or ::marker.

@othermaciej
Copy link
Member

If you think of pseudo-elements and pseudo-classes as analogies for elements and classes, it's the case that an element has only one tag name and multiple class names. So perhaps it is right that any given element should only have up to one part name, and optionally multiple state names.

Going back to the calendar example, you'd probably have to do it as <day class="weekday Saturday week-1 us-holiday>. It is perhaps over-engineering to let pseudo-elements have multiple pseudo-element "tag names", since markup itself doesn't have that. While it would be neat, it seems like the world where there's only one tag name has generally be livable. And built-in UA pseudo-classes for form controls don't use multi part naming either.

Taking that direction, there'd be no need for ::part(day us-holiday), instead we just need to make sure ::--day:--us-holiday works.

@plinss
Copy link
Member

plinss commented May 8, 2020

To be clear, under my current proposal, your example would be: <day part="day" part-class="weekday Saturday week-1 us-holiday">, a 'class' attribute would not be exposed on the part but could be used within the custom element normally.

And it would be selected as: ::--day.us-holiday, the pseudo-class notation would be used to select custom state of the part, e.g. ::--day.us-holiday:--checked.

I believe this satisfies all the use cases, brings shadow part selector syntax into alignment with custom pseudo-classes, and allows the extension I mentioned above of allowing custom elements to potentially create built-in pseudo elements as well. And, like custom state is now really 'custom pseudo-classes', it turns shadow parts into 'custom pseudo-elements'.

@tabatkins
Copy link
Member Author

But by using ::part you're shunting all shadow parts into being a certain type of pseudo-element, rather than allowing custom elements to create their own type of custom pseudo-elements.

No, in the bit you quoted for this response I was referring to my OP (original post), where part="--foo --bar" gives you ::--foo and ::--bar, and also ::part(--foo --bar) if you need it.

If you think of pseudo-elements and pseudo-classes as analogies for elements and classes, it's the case that an element has only one tag name and multiple class names. So perhaps it is right that any given element should only have up to one part name, and optionally multiple state names.

I think it's important not to blindly hew too close to DOM structures - examine why we have them and what value they bring.

Tagnames exist because elements have to have a particular identity and behavior; you can't mix-and-match them. It's a required bit of creating an element, so the browser knows whether the element should be link-like or button-like or list-like, etc.

But we're not doing that; we're providing ways for component users to select elements. And from Selector's POV, there is no meaningful difference between a tagname and a class name - they're both just idents associated with an element. Because DOM only allows an element to have one tagname (for the aforementioned reason), tagname selectors are given a syntax that doesn't allow you to specify multiple of them together in a compound selector. But other than that, identical.

So Selectors doesn't provide any reason to create a tagname-like semantic. Is there any value besides that to specially choosing one of the part names as the "tagname" equivalent and making the rest "class name" equivalents? I don't think there is; it seems just as reasonable to select a ::--day as a ::--monday or ::--weekend, etc. I haven't heard anyone provide an explanation of the value so far either, besides "well, elements have tagnames and the ::before kinda looks more like a tagname than a classname".

And note that this corresponds well to the existing selectors authors write for their own component hierarchies, which are solely class-based. They don't privilege one name with a special syntax.

instead we just need to make sure ::--day:--us-holiday works.

I'll refer you again to #4900 (comment) - letting a component give custom pseudo-classes to its parts is going to be very fraught, because the sub-component itself (written by a non-cooperating party) is also trying to give custom pseudo-classes to itself.

Unless you want to navigate the briar patch I outline in the linked comment, selecting based on states coming from the outer component and states from the inner component must use a different syntax.

So, since we're already on the hook for two syntaxes (part names from the outside, custom pseudo-classes from the inside), I think we need a really good justification for increasing that to three (two slightly different notions of "part name").

@plinss
Copy link
Member

plinss commented May 9, 2020

I think it's important not to blindly hew too close to DOM structures - examine why we have them and what value they bring.

It's also important to leverage the existing architecture of the platform and not invent new things when what we have already works.

Tagnames exist because elements have to have a particular identity and behavior; you can't mix-and-match them. It's a required bit of creating an element, so the browser knows whether the element should be link-like or button-like or list-like, etc.

And pseudo-elements already have a particular identity and behavior, giving a pseudo-element a name is part of creating a new pseudo-element.

But we're not doing that; we're providing ways for component users to select elements. And from Selector's POV, there is no meaningful difference between a tagname and a class name - they're both just idents associated with an element. Because DOM only allows an element to have one tagname (for the aforementioned reason), tagname selectors are given a syntax that doesn't allow you to specify multiple of them together in a compound selector. But other than that, identical.

There are differences in the selectors, specificity. There's also a semantic component in the tag name and pseudo-element name.

So Selectors doesn't provide any reason to create a tagname-like semantic. Is there any value besides that to specially choosing one of the part names as the "tagname" equivalent and making the rest "class name" equivalents?

The value is matching the existing platform rather than inventing something new for no good reason. We already have tag names, pseudo-element names, and classes. Why invent a new 'part name' which is sort of like a class but isn't a class, and isn't selected like a class?

I don't think there is; it seems just as reasonable to select a ::--day as a ::--monday or ::--weekend, etc. I haven't heard anyone provide an explanation of the value so far either, besides "well, elements have tagnames and the ::before kinda looks more like a tagname than a classname".

Because a 'monday' is a kind of 'day' as is a 'weekend' day, or a 'holiday', there is semantic value here. Allow authors to express that.

Pseudo-elements are also structural, just not structure that's exposed in the DOM, but structure nonetheless that's exposed to selectors. Structural nodes have type names, they may additionally have classes. Class names are not structural, they're descriptive.

Given your calendar widget example, imagine week parts as well as day parts. You could select the third day of the second week as:
::--week:nth-of-type(2)::--day:nth-of-type(3)
rather than:
::--second-week::--tuesday
while the first syntax is more verbose, it's also more clear what's going on. It also doesn't rely on decorating each week with a 'first-week', 'second-week', etc and each day with 'monday', 'tuesday', etc, normal structural selector math just works. (And is Tuesday the second or third day of the week in your locale?)

The other aspect here is cow-path paving. Custom elements are supposed to allow web authors to develop new elements that act as much like native elements as possible. Shadow parts as currently proposed only allow custom element authors make a specific kind of pseudo-element (a 'part', which is as semantically valuable as a div) and then use a new and unique way of selecting them.

Some of these inventions are intended to be brought into the platform. If your example calendar widget becomes part of HTML, it would have a ::day pseudo-element, not a ::part(day) pseudo-element, because that's the way the platform works. Maybe that ::day could also have a ::day(weekend) syntax, but that could just as easily be ::day.weekend. Let's give custom element authors the ability to create custom pseudo-elements of a type that they determine, that look like, and act like, built-in pseudo-elements. And as I mentioned above, it also potentially gives them a way to create native pseudo-elements as well. Something that the existing shadow part proposal can't do. Wouldn't it be useful to allow custom elements to define their own ::marker for example? That just behaves in lists like any other marker? Or placeholder text that behaves, and can be styled, just like the placeholder text in native input elements right next to it?

Yes, exposing classes on top of pseudo-elements is something new (tho it's not a new concept, like part names is). There's also no reason that native pseudo-elements can't do the same. I expect once we start thinking in those terms we'll come up with several uses, e.g. ::error.spelling and ::error.grammar rather than ::spelling-error and ::grammar-error.

So, since we're already on the hook for two syntaxes (part names from the outside, custom pseudo-classes from the inside), I think we need a really good justification for increasing that to three (two slightly different notions of "part name").

You're already invented a third thing with the existing part name proposal, ::part(weekend):--selected already uses three concepts, pseudo-element name, part name, and pseudo-class. ::--day.weekend:--selected uses three existing concepts, nothing new. There's also no semantic value to ::part, just as there was no additional value in having :state().


I think the disconnect we have is that you're thinking of this feature as "just a mechanism to select parts of a custom element, who cares how", and I'm thinking of it as a way of creating custom pseudo-elements (and explaining the built-in ones).

@tabatkins
Copy link
Member Author

Given your calendar widget example, imagine week parts as well as day parts.

Again, nesting parts isn't possible right now and we probably don't want to make it possible. It exposes internal structure that we probably want to hide.

You're already invented a third thing with the existing part name proposal, ::part(weekend):--selected already uses three concepts, pseudo-element name, part name, and pseudo-class.

::part() isn't a third thing for the authors of the component to decide on (or for the users of the component to have to remember which values correspond to it). It's a syntax hack over the pseudo-element name.

And as I mentioned above, it also potentially gives them a way to create native pseudo-elements as well. Something that the existing shadow part proposal can't do. Wouldn't it be useful to allow custom elements to define their own ::marker for example?

As far as I can tell, my proposal is perfectly compatible with this idea? What makes you think it isn't?

@plinss
Copy link
Member

plinss commented May 9, 2020

Again, nesting parts isn't possible right now and we probably don't want to make it possible. It exposes internal structure that we probably want to hide.

While it doesn't have to be a V1 feature, there's no reason to not allow custom element authors to expose whatever structure they want to, so long as it's a choice, not a side-effect of implementation. As I showed in my example, that structure can be useful. I'm not trying to expand this issue into nested parts, but it's worth seeing how these selectors play in a world where nested parts exist.

::part() isn't a third thing for the authors of the component to decide on (or for the users of the component to have to remember which values correspond to it). It's a syntax hack over the pseudo-element name.

But part names are a completely new thing. I expect the majority of custom pseudo-element will only have a single name, so no additional cognitive load on authors. If they're designing something with multiple names, it may be a good idea to make them think about why and what those names mean, they are creating an implementation contract after all.

Also, custom element authors already understand the concept of minting element names, and using tags vs attributes (e.g. should it be <foo> and <bar> or <thing type="foo"> vs <thing type="bar">, same concept). I expect classes won't be a surprise to them either. Tag names and classes also aren't anything new for the consumers of those elements. Whereas part names that are sort of tag-like and sort-of class-like are a new concept are are more likely to cause confusion than clarity, especially if they have a new syntax to boot. I know the idea was surprising to me, and I've been doing this for a while...

All existing pseudo-elements are clearly explainable as having a single tag-like name. Class-like part names don't directly map onto anything else in the platform.

And I'm glad we agree that :part() is a hack :-P

As far as I can tell, my proposal is perfectly compatible with this idea? What makes you think it isn't?

I suppose it is, I admit I'm having trouble keeping all the proposals straight. In my head, your proposal is still conflating part names with pseudo-element type and a class-like thing, so while doable, it feels hacky to me. In mine the pseudo-element type is explicit and only used for that. In my experience, when we mix concepts we tend to get bitten somewhere down the line.

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Aug 21, 2020

And it would be selected as: ::--day.us-holiday, the pseudo-class notation would be used to select custom state of the part, e.g. ::--day.us-holiday:--checked.

That would require substantial changes to the selector grammar, and at that point, it might be easier to convert :: (and <ws>::) into proper‑ish combinators1.

Footnotes

  1. https://github.com/w3c/csswg-drafts/issues/5472

@annevk
Copy link
Member

annevk commented Nov 18, 2022

Since ::part() has shipped I guess this ought to be closed?

@tabatkins
Copy link
Member Author

While several of the larger changes are likely frozen out of possibility now, the OP's suggestion of allowing ::--foo as an alternate spelling of ::part(foo) is still potentially doable if someone's interested in it.

@annevk
Copy link
Member

annevk commented Nov 21, 2022

But is that worth doing if you're not also solving ::part(foo bar)? Hmm.

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

8 participants