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

Support Custom Pseudo-elements #300

Closed
hayatoito opened this issue Sep 1, 2015 · 74 comments
Closed

Support Custom Pseudo-elements #300

hayatoito opened this issue Sep 1, 2015 · 74 comments

Comments

@hayatoito
Copy link
Contributor

See the proposal from @philipwalton.
https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Custom-Pseudo-Elements.md

Let me file an issue here to discuss and keep track of the proposal.

@hayatoito
Copy link
Contributor Author

At the first glance, the proposal sounds good to me.
I think we would have more questions and answers for detail, however, before proceeding, I'd like to hear more opinions from other people.

@tabatkins, WDYT if you have a chance to take a look?

@rniwa
Copy link
Collaborator

rniwa commented Sep 1, 2015

We would prefer using part syntax as follows so that future pseudo element names we introduce in CSS wouldn't interfere with author defined parts components expose:

<date-range-selector>
  <!-- #shadow-root -->
    <div id="container">
      <input part="start-date" id="start-date" type="date">
      <input part="end-date" id="end-date" type="date">
    </div>
  <!-- /shadow-root -->
</date-range-selector>
date-range-selector:part(start-date),
date-range-selector:part(end-date) {
  /* normal styles */
}

@hober @othermaciej

@tabatkins
Copy link

Yeah, if we do add custom pseudo-elements, we need to do it namespaced, like ::part.

That said, the CSSWG provisionally accepted my @apply rule draft, which makes pseudo-elements unnecessary for WC.

That is, CSS variables handle most of the styling needs of WC already. They only fall down when you want to offer the ability to arbitrarily style an element; there are too many properties to make it reasonable to offer enough variables for styling. Pseudo-elements expose an element for arbitrary styling, so that's useful. The @apply rule also does that, tho, and via the existing mechanism of CSS custom properties, so there's probably no need to add custom pseudo-elements.

@philipwalton
Copy link
Contributor

@tabatkins I'm aware of the @apply rule (it was discussed in the issue that prompted this proposal), and I think in general using @apply with custom properties is the best way to style third-party elements.

However, I brought up several use-cases in the proposal that I think are legitimate and that I don't think the @apply rule can handle (unless I'm misunderstanding part of its functionality).

You said:

Pseudo-elements expose an element for arbitrary styling, so that's useful. The @apply rule also does that [...]

Can you go into more detail here? How would the @apply rule handle the styling of an input element only in the :focus, :enabled, and :out-of-range state? As far as I can tell, the only way is for the element author to hard-code that selector in the element's shadow styles. Something like:

input {
  /* normal input styles */
  @apply --date-range-selector-inputs;
}
input:focus:enabled:out-of-range {
  /* input styles for the focus, enabled, and out-of-range states */
  @apply --date-range-selector-inputs-focused-enabled-out-of-range;
}

It seems unreasonable for component authors to have to anticipate and hard-code every element state their users may ever want to style. Exposing the element itself is much simpler and still maintains the privacy of other element in the component (unlike ::shadow/>>>).

@tabatkins
Copy link

You're right, this doesn't handle pseudo-classes well. I'll have to give that some thought.

@morewry
Copy link

morewry commented Sep 28, 2015

I'm just a user, but, in my opinion, there is a need for pseudo-element functionality from a perspective of allowing a component author to provide a stable public interface for styling a component. As a component author, I should be able to change the internal implementation details of my web component without any disruption to a component user. The only way I can feasibly do that is to have an explicit mechanism for defining my publicly style-able surface. Custom pseudo elements do this. While @apply appears to actually do so as well, custom pseudo elements have benefits beyond lack of difficulty with pseudo classes.

Vendor elements that utilize obscured internals have long allowed styling of certain portions via pseudo elements. And, correct me if I'm wrong, isn't there some work on standardizing these for common replaced elements, like form controls? Therefore, with custom pseudo elements, a user authored component would be style-able like a vendor component. Having user authored elements feel like native elements is a good thing--and not only because having the standard explain the existing platform is nice.

A component user does not have to learn any new language constructs to style a custom pseudo element, and yet can still enhance their use of a custom pseudo element with new CSS features as they develop. While I am enthusiastic about bringing popular pre-processor features (and implied enhancements, like @apply) to CSS, I am also of the opinion that, in respect to web component styles, we should lean toward allowing CSS authors to simply...write familiar CSS. Since several of the CSS standards are brand spanking new, surely they would have kinks needing to be worked out? As a result, it seems to me that it would be putting the cart before the horse to hang the style story for Shadow DOM on them.

@tabatkins
Copy link

Efforts to standardize the internal pseudo-elements aren't very successful, unfortunately. I keep trying to get something done with that, but it's very difficult to do in a reasonable way. I'm not confident that it will ever be achieved.

Mixins and Nesting are battle-tested concepts with long histories from CSS preprocessors; their mixture, especially, has a long history of use in Sass (and maybe more?). These aren't new concepts, they're just new to vanilla CSS. And they lean on existing useful and well-tested concepts from CSS, like inheritance and properties.

Custom pseudo-elements, on the other hand, don't. We've had pseudo-elements for a long time, but only a small number, and they don't nest. We still need to figure out how to solve a number of issues before it's usable:

  1. Are shadow-pseudos more like classes or IDs? That is, do they need to be unique, or is it ok to have multiple selected by the same name? If you can have multiple, how do we select a single one to style? Does the component author have to allow this? Can a single element expose multiple pseudo names for itself?
  2. How do we handle both a parent and child exposing themselves as pseudo-elements? Is the nesting visible or not? If yes, can you write ::foo::bar?
  3. (This one's important and hard.) If a component contains other components, and wants to expose some of its sub-components parts as pseudo-elements, how does it surface them? Are they selectable by default somehow (if so, how do you handle namespacing)? If not, how do you expose them? Is it apparent that they come from a sub-component, or can the parent component make them look "native"?

All of these questions apply to vars/@apply/nesting too, but they all have definite, simple answers there, and those answers are imo reasonable behavior. We have no idea what the answers would be for pseudo-elements, tho.

This is why it's backwards to characterize vars/@apply/nesting as "new and untested" and pseudo-elements as "old and well-known". It's almost exactly the opposite when you dig into details.

@philipwalton
Copy link
Contributor

Are shadow-pseudos more like classes or IDs? That is, do they need to be unique, or is it ok to have multiple selected by the same name? If you can have multiple, how do we select a single one to style? Does the component author have to allow this? Can a single element expose multiple pseudo names for itself?

I imagined them as being non-unique, like classes. It's extremely likely that component authors will create elements with many children of the same type, like a <ul> element does today.

Though that raises the question (at least in my mind) of whether or not to allow a single HTML element to have more than one pseudo element name. To continue with the <date-range-selector> element example, the start and end date inputs are both inputs, and will probably want to be styled similarly, but they also might want to be styled individually. Should it be possible to select the start date field via both ::start-date and ::field (or whatever). Perhaps by allowing space-separated pseudo attribute name definitions: e.g. <input pseudo="field start-date">.

How do we handle both a parent and child exposing themselves as pseudo-elements? Is the nesting visible or not? If yes, can you write ::foo::bar?

(This one's important and hard.) If a component contains other components, and wants to expose some of its sub-components parts as pseudo-elements, how does it surface them? Are they selectable by default somehow (if so, how do you handle namespacing)? If not, how do you expose them? Is it apparent that they come from a sub-component, or can the parent component make them look "native"?

I think this paradigm can be simplified to just components and their sub-components, rather than thinking of it in terms of a possibly infinitely nested tree of shadow components (ala /deep/). As long as the component author exposes the sub-component via a custom pseudo-element, it shouldn't matter how many levels deep it's nested. For example:

<x-foo>
  <!-- #shadow-root -->
    <x-bar pseudo="x-bar-item">
  <!-- /shadow-root -->
</x-foo>

If <x-bar> exposes its own ::x-baz-item pseudo-element, then consumers of <x-foo> could theoretically style ::x-baz-item pseudo-elements from a main document stylesheet via something like:

x-foo::x-bar-item::x-baz-item {
  color: red;
}

This paradigm allows for /deep/-like styling without the performance implications or the exposing of component internals.

@philipwalton
Copy link
Contributor

As I was writing that last post, I thought of a possible solution to the pseudo-class/@apply issue. Borrowing from Sass, again, pseudo-element states could be defined in the custom property definition via the &:pseudo-class nesting syntax.

Here's what it could look like:

:root {
  --x-foo-styles: {
    color: blue;

    &:hover {
      background-color: yellow;
    }
    &:active {
      outline: 1px dotted;
    }
  }
}

This, if @apply-ed in a shadow <style> declaration, would only be applied when the pseudo-class states matched.

There are definitely some specificity/cascade issues to iron out, but if this could work, then I believe all my concerns about custom property shortcomings would be alleviated.

@rniwa
Copy link
Collaborator

rniwa commented Sep 29, 2015

Like we agreed during the last F2F, custom properties, @apply, and custom pseudo elements aren't conflicting ideas. They address slightly different use cases and both can be supported.

In particular, in an isolated web component case where neither component nor its container document trusts each other, we can't expose all mixins defined in the container document into the shadow tree.

Also, there is an added benefit of developer familiarity with custom pseudo elements. While the technical problems faced by nested rules and @apply might be similar (or even simpler as you claim) to those faced by custom pseudo elements, relying solely on three brand new CSS syntax seems like a lot of cognitive load.

@rniwa
Copy link
Collaborator

rniwa commented Sep 29, 2015

Also, where is the proposal for nesting syntax? Is it https://lists.w3.org/Archives/Public/www-style/2011Jun/0022.html ?

@othermaciej
Copy link

I think custom pseudos are a good direction. Comments on some specific issues:

(1) It's critical for component authors to be able to whitelist the set of style properties that can be applied to an exposed named part.
(2) I'm somewhat partial to :part syntax because then we never have to worry about namespace collisions with future CSS built-in pseudos.
(3) I don't think prefixing is necessary. If pseudo names are reused between different custom elements, that's not a problem, because you can specify the element and part.
(4) I don't think @apply plus the increasing series of other features that need to come along with it is a replacement. The syntax is pretty inscrutable. I immediately understand what Philip's named part examples are doing, but I don't understand what his @apply + nesting proposal does.
(5) Having a nice and easy to understand syntax for consumers of a component is important. Unlike many other aspects of web components, this syntax will be used by many garden-variety web developers, not just authors of JavaScript frameworks.

@tabatkins
Copy link

@philipwalton (re: nesting) Yes, that's precisely what I was getting at when I was talking about @apply/nesting. ^_^ There's no specificity issues to iron out - It Just Works™.

@rniwa

Like we agreed during the last F2F, custom properties, @apply, and custom pseudo elements aren't conflicting ideas. They address slightly different use cases and both can be supported.

No, they don't address slightly different use-cases. They address the exact same use-cases.

In particular, in an isolated web component case where neither component nor its container document trusts each other, we can't expose all mixins defined in the container document into the shadow tree.

Sure we can. And when you do want to block, that's what all: initial is for. (Alternately, an equivalent of all for just custom properties has been proposed; it would be called --, as in --: initial.) You can then let particular custom properties thru with --foo: inherit.

Also, there is an added benefit of developer familiarity with custom pseudo elements. While the technical problems faced by nested rules and @apply might be similar (or even simpler as you claim) to those faced by custom pseudo elements, relying solely on three brand new CSS syntax seems like a lot of cognitive load.

I already addressed this in my earlier reply to Rachel.

@othermaciej

(1) It's critical for component authors to be able to whitelist the set of style properties that can be applied to an exposed named part.

Why? CSS doesn't do this today. ::before/after take all properties. ::first-line and friends don't, but that's because they're not actually elements, they're bizarre collections of fragments (to use CSS terms), and that limits what you can reasonably do with them. That's not relevant to this topic, tho; we're only planning to expose actual elements here.

If you want to whitelist properties, that's best done with simple CSS variables - expose variables for each property you want to allow. If you want to expose arbitrary styling but blacklist some properties for sanity, that's trivially done with @apply - just list the blacklist properties with their desired values after the @apply rule. With custom pseudos, this is yet another new thing you'll have to define and introduce, presumably in JS.

(2) I'm somewhat partial to :part syntax because then we never have to worry about namespace collisions with future CSS built-in pseudos.

Yes, ::part() is definitely a preferable syntax for pseudo-elements here.

(4) I don't think @apply plus the increasing series of other features that need to come along with it is a replacement. The syntax is pretty inscrutable. I immediately understand what Philip's named part examples are doing, but I don't understand what his @apply + nesting proposal does.

It's not "increasing", it's complete. Variables/@apply/nesting are a complete styling feature.

The syntax is relatively new, but a lack of immediately familiarity doesn't automatically imply "inscrutable". It's a declaration block stuffed into a custom property. Nesting is familiar to users of every single CSS preprocessor in existence, and is consistently one of the most popular and highly used features. The learning curve here is miniscule based on practical evidence.

@rniwa
Copy link
Collaborator

rniwa commented Sep 30, 2015

Sure we can. And when you do want to block, that's what all: initial is for. (Alternately, an equivalent of all for just custom properties has been proposed; it would be called --, as in --: initial.) You can then let particular custom properties thru with --foo: inherit.

That's such a cumbersome API. So if an author wanted to use a component and didn't want to expose any custom properties to it, then he/she has to do --: initial and selectively expose custom property. That's too much complexity just to style a part of component.

Nesting is familiar to users of every single CSS preprocessor in existence, and is consistently one of the most popular and highly used features. The learning curve here is miniscule based on practical evidence.

I disagree. Nesting declarations of pseudo-class selectors inside custom mixins is extremely confusing.

@philipwalton
Copy link
Contributor

That's such a cumbersome API. So if an author wanted to use a component and didn't want to expose any custom properties to it, then he/she has to do --: initial and selectively expose custom property. That's too much complexity just to style a part of component.

@rniwa, what would the custom pseudo-element equivalent be for whitelisting the styleable properties? As the proposal is now (and obviously it can change), exposing an element as a custom pseudo element exposes all properties. Presumably component authors could use !important to blacklist, but that wouldn't work to whitelist.

@philipwalton
Copy link
Contributor

There's no specificity issues to iron out - It Just Works™.

@tabatkins, yeah I think you're right. I'd originally imagined a situation where element-author-defined pseudo-class rules would trump @apply-ed rules (inside a selector without the pseudo-classes), but that issue could easily be solved by moving the @apply declaration to after all rules with pseudo-classes.

@rniwa
Copy link
Collaborator

rniwa commented Sep 30, 2015

@philipwalton : My point in #300 (comment) is nothing to do with whitelisting properties but more to do with exposing mixins and properties across component boundaries.

In the case of custom pseudo elements, the authors of components are explicitly opting in the contract that a part of its component is stylable by its user, and the users of components are similarly opting in to style those parts without exposing any other mixin or custom property defined in the document.

This is a crucial property for the isolated components where neither component author nor component user trust each other (e.g. for cross-origin widgets).

@tabatkins
Copy link

That's such a cumbersome API. So if an author wanted to use a component and didn't want to expose any custom properties to it, then he/she has to do --: initial and selectively expose custom property. That's too much complexity just to style a part of component.

I'm not seeing the complexity. Did you actually write it out and see what it would look like, compared to whatever else you'd want to do?

/* set one variable for a sub-component,
   letting the outer page set the rest if they want */
sub-component { 
  --heading: { color: blue; text-decoration: underline; };
}

/* set one variable for a sub-component,
   and force the rest to be default value,
   blocking the outer page from setting anything */
sub-component {
  --: initial;
  --heading: { color: blue; text-decoration: underline; };
}

I'm not seeing the "too much complexity".

And note, we're comparing this to a nonexistent, unknown syntax for exposing or hiding the ::part()s of sub-components. We have no idea if such a syntax will be blacklist or whitelist based, how it will be invoked, or what language we'll use for it. (HTML attributes? JS api? CSS syntax?) We have no clue what its complexity will be, because it doesn't exist yet. This is one of many already-mentioned unanswered-so-far questions about the ::part syntax.

I disagree. Nesting declarations of pseudo-class selectors inside custom mixins is extremely confusing.

You're free to disagree, but heavy usage of nesting in Sass and others goes against your feelings. I'm not sure what the usage numbers are for the nearest direct analogue (Sass nesting within @mixin rules), but this doesn't look confusing in the slightest to me:

sub-component {
  --heading: {
    color: blue;
    text-decoration: underline;
    &:hover { color: red; font-weight: bold; }
  };
}

Compare this to an assumed ::part-based syntax:

sub-component::part(heading) {
  color: blue;
  text-decoration: underline;
}
sub-component::part(heading):hover {
  color: red; font-weight: bold;
}

Those look basically identical. I won't fault ::part() for the extra selector verbosity there; if Nesting exists, it can be simplified to a basically identical form:

sub-component::part(heading) {
  color: blue;
  text-decoration: underline;
  &:hover { color: red; font-weight: bold; }
}

I just want to emphasize, again, that ::part() has a number of currently-unanswered questions about its functionality and syntax. I outlined several of them up above in a previous comment. They certainly can be addressed, but haven't been so far, and when they are, several of them will imply further syntax and complexity for the feature. Custom properties and @apply/Nesting, on the other hand, have answers to all those questions right now; they fall out of the definitions of the feature.

For more complex cases, until we actually answer the relevant questions for ::part (such as how sub-component ::parts are exposed or hidden), we won't have the ability to do good comparisons with anything else.

@tabatkins
Copy link

This is a crucial property for the isolated components where neither component author nor component user trust each other (e.g. for cross-origin widgets).

Fully isolated components are a trivial case in the vars/apply/nesting feature set; you just don't use any undefined variables. If you only use variables you define yourself, there's no way for outside-world inheritance to interfere. I presume this is the same as in ::part - just don't expose any parts, and nobody can style you.

If you further want to block sub-components from receiving styling, this is trivial in vars/apply/nesting, as I demonstrated above (or if you want to block everything immediately, for all your sub-components, just apply a :host { --: initial; }, or maybe just rely on the "no inheritance by default/ever" concept that I assume isolated components will have).

Since we have no idea how ::part will expose/hide the parts of sub-components, we can't make a proper comparison yet, but we can at least see that the v/a/n option is trivial.

@rniwa
Copy link
Collaborator

rniwa commented Sep 30, 2015

Well, the problem is that we do need the fidelity of being able to expose some parts of the isolated component. This is the exact use case addressed by various builtin pseudo elements in WebKit. Since that is a feature that has been shipping in WebKit/Blink for years and proven to be popular amongst developers, I don't see why we need to re-invent the wheel to replace that solution.

@tabatkins
Copy link

Yes, I understand that we need to expose particular parts for styling; that's addressed equally well with v/a/n or ::part. (With v/a/n, you document several variables, and use each within an appropriate selector, like h1 { @apply --heading; }. With ::part(), you expose the relevant element as a visible part, I presume with something like <h1 part=heading>.)

What, specifically, do you think is hard with one or the other solution?

I don't see why we need to re-invent the wheel to replace that solution.

No matter what we do, we're going to be reinventing things. We will not produce a solution that allows for a 100% fidelity recreation of the current form pseudo-element stuff, because that immediately runs into namespace issues. Current form controls also only touch on a small subset of the possibility space, while ::part() needs to address a bunch more cases (I outlined some in a previous comment).

So lots of invention have to happen no matter what.

@zamfofex
Copy link

zamfofex commented Jun 9, 2016

From my point of view, custom elements are here to give authors the same power as user-agents have of defining elements, or at least as close to that as possible.

The elements defined by the spec are not necessarily implemented with JavaScript and CSS, but they could be. Even the most convoluted ones like input with all its types and shapes.

Giving authors the ability to create custom elements is putting them at nearly the same level as the user-agent, allowing them to define elements in the same way the user-agent does.

Not without giving authors the power to create custom pseudo-elements feels incomplete to me, as it’s yet another thing the user-agent can do that the author cannot. By giving authors the ability to create custom pseudo-elements you are bringing their power closer to the user-agent’s.

@tabatkins effectively, what I’m saying is that I prefer custom pseudo-elements to your suggestion because it is closer to what is done by user-agents to regular elements.

I suggest the following syntax:

x-foo..custom-element
{
    /* styles */
}

As it’s comparable to what we have today:

/* user-agent-defined class */
button:enabled
{}

/* author-defined class */
button.loading
{}

/* user-agent-defined pseudo-element */
x-foo::first-line
{}

/* author-defined pseudo-element */
x-foo..last-letter
{}

@andyearnshaw
Copy link

andyearnshaw commented Jun 10, 2016

As it’s comparable to what we have today:

That comparison doesn't really hold true for web components. "User-agent-defined [classes]" (pseudo-classes) represent the target when it is in a particular state without modifying the attributes of the element. Component authors have a requirement similar to this, as opposed to page authors who are using the component. Page authors expect that, when the internal state of an element changes (e.g. focus), its attributes do not (though its properties may). A component author, therefore, wouldn't modify the class attribute of the custom element hosting its shadow tree.

In a similar vein, a component author might wish to define a "pseudo-element" to represent an internal part of the component, which they would do without modifying the host element in any observable manner. A page author would just add a child element to the host.

/* user-agent-defined state */
button:enabled

/* page author-defined state */
button.loading

/* component author-defined state */
x-button<???>

/* user-agent-defined pseudo-element */
foo::first-line

/* page author-defined element */
foo bar

/* component author-defined shady-element */
x-foo<???>

If you want to propose new syntax, you should rethink your premise for choosing .. (which I don't really like, to be honest).

@zamfofex
Copy link

@andyearnshaw

A component author, therefore, wouldn't modify the class attribute of the custom element hosting its shadow tree.

Hrn, you’re right. Either way, my point is: I really dislike the ::part() syntax. I think a different token could be used to represent custom pseudo-elements.


But does anyone disagree with what I said about custom pseudo-elements?

Effectively, what I’m saying is that I prefer custom pseudo-elements to [@tabatkins’] suggestion because it is closer to what is done by user-agents to regular elements.


Addressing #300 (comment):

(1) Are shadow-pseudos more like classes or IDs?

I think they should act exactly like ids.

invalid:

<div pseudo="foo bar"></div>

invalid:

<div pseudo="foo"></div>
<div pseudo="foo"></div>

(2) How do we handle both a parent and child exposing themselves as pseudo-elements?

<div pseudo="foo">
    <div pseudo="bar"></div>
</div>
/* matches nothing */
x-baz::foo > div
{}

/* matches nothing */
div > x-baz::bar
{}

/* matches nothing */
x-bar::foo::bar
{}

/* matches <div pseudo="bar"> */
x-baz::foo > x-baz::bar
{}

(3) If a component contains other components, and wants to expose some of its sub-components parts as pseudo-elements, how does it surface them?

You can access them by nesting ::. The following would show a “Hello, world!” button.

<!doctype html>
<html>
    <head>
        <style>
            x-foo::bar::baz::before
            {
                content: "Hello, world!";
            }
        </style>
    </head>
    <body>
        <x-foo>
            #shadow
                <x-bar pseudo="bar">
                    #shadow
                        <button pseudo="baz"></button>
                    /#shadow
                </x-bar>
            /#shadow
        </x-foo>
    </body>
</html>

@matthewp
Copy link

How would you "forward" a part, I don't quite understand that.

@tabatkins
Copy link

Say you're using an <x-button> component in your own shadow tree, and it exposes a "label" part that you'd like to expose as part of your own API. You can do <x-button part="label => button-label">, and now ::part(button-label) will work on you, and target the <x-button>s ::part(label).

@matthewp
Copy link

Can you only forward a single part, then?

@tabatkins
Copy link

No, the part attribute takes a comma-separated list of commands. https://tabatkins.github.io/specs/css-shadow-parts/#part-attr

@madeleineostoja
Copy link

Zero-arg mixins are convenient, but all the real useful mixins, in my experience, use arguments; you can only do pretty simple things with zero args

True, but I still think there's an argument to be made for theming with @apply since it works across both components/shadow roots and light DOM (vs. using classes for light DOM and ::theme for components).

Without @apply I imagine a lot of people (myself included) would fall back on mixins from their pre/post-processor of choice to achieve that kind of thing, so I guess the question is whether that should be baked into the platform or left to tooling?

@othermaciej
Copy link

I anticipated this reaction, and I probably should have been more explicit about what it's doing: ::theme() is exactly the same, mechanically, as @apply. Literally, modulo some unimportant corners of the functionality, the two are doing precisely the same thing - letting you target a component arbitrarily far down in the shadow hierarchy.

I may not understand @apply very well. But it seems like @apply puts control in the hands of component authors for where stuff gets applied, and the expected API contract is that the client of the component provides some custom properties with the intended styles. But @theme applies style to all parts with a given name everywhere. And the component client has to decide whether to use @part or @theme. So the component's API contract is not just a specific name of the styling hook, but also the requirement to know whether they should use ::part or ::theme with it.

Meanwhile, forwarding provides a completely component-controlled way of handling it where the client should always just say ::part.

Components authored with a closed shadow DOM will have to be done with forwarding, and in that case ::theme is useless. So different kinds of components will have different API contracts for how to style them that are dependent on an authoring choice that shouldn't be relevant to this.

In brief, it seems like telling your users to use ::theme (or leaving the choice to them) is poor authoring practice, and explicitly forwarding part names is good practice. The fact that ::theme theoretically is similar in power in some sense to @apply is not a good reason to have it. Depth of styling should be controlled by the component, not the client of the component, and leaving it to client code adds a needless confusing decision.

@tabatkins
Copy link

I may not understand @apply very well. But it seems like @apply puts control in the hands of component authors for where stuff gets applied, and the expected API contract is that the client of the component provides some custom properties with the intended styles. But @theme applies style to all parts with a given name everywhere. And the component client has to decide whether to use @part or @theme. So the component's API contract is not just a specific name of the styling hook, but also the requirement to know whether they should use ::part or ::theme with it.

Not really. Like I said above, ::theme() is actually the almost-direct translation of @apply's functionality back into the selector space (where this functionality belongs). Every bad implication you can imagine ::theme() having, @apply has, because inheritance lets you set the custom property arbitrarily far up in the flat tree, and it'll work it's way down to the component unless explicitly blocked (by the custom property getting set by an element somewhere between the component and the first element to set it).

::part(), on the other hand, is the better-designed API that you actually want. It, plus part-forwarding, gives you access to precisely the parts that your component chooses to give to you, and it works consistently between open and closed shadows.

If we all collectively decided that we only wanted ::part(), that's okay with me. It's the good part of the proposal anyway. I included ::theme() because it avoids throwing away any power; this power is precisely what some Polymer people talked about as useful in their early feedback to me. And if we don't allow ::theme(), we don't actually shut anything down, we just make similar usage vastly less convenient (the "big bag of custom properties" approach I talk about in the spec).

Ultimately, if you can pass a single value arbitrarily far down the flat tree without the intervening elements having to do anything (besides just fail to stop you), it seems weird to not allow sets of values; disallowing it doesn't actually add any security whatsoever, just makes it less convenient for component authors and users.

The fact that ::theme theoretically is similar in power in some sense to @apply is not a good reason to have it.

Just to reiterate, it's not similar "in some sense" - it's exactly as powerful in a direct-translation sense - @apply and ::theme() are mirrors of each other, just living in different parts of the CSS syntax space. And @apply is only very slightly more powerful var(), just way more convenient for the use-case of allowing arbitrary styling on an element (only "more power" is that it allows setting arbitrary custom properties, which can't have their names predicted ahead of time).

@rniwa
Copy link
Collaborator

rniwa commented Feb 13, 2017

Just to reiterate, it's not similar "in some sense" - it's exactly as powerful in a direct-translation sense - @apply and ::theme() are mirrors of each other, just living in different parts of the CSS syntax space. And @apply is only very slightly more powerful var(), just way more convenient for the use-case of allowing arbitrary styling on an element (only "more power" is that it allows setting arbitrary custom properties, which can't have their names predicted ahead of time).

"exactly as powerful" is a misleading statement given @apply can be used without any shadow trees even if there was an equivalent expressibility when it comes to styling parts of components across shadow boundaries. I can see some people may want to be using mix-ins outside the context of shadow trees, so for them @apply is a lot more useful than ::theme which seems to only work on parts defined inside a shadow tree.

Having said that, we're all for focusing on ::part first regardless of what happens to @apply vs ::theme since ::part is the feature what we always wanted.

@matthewp
Copy link

I disagree that ::theme is unimportant. Having very deep shadow trees is going to be very common. If you have to manually forward each part at every level it is going to get quite verbose. I can imagine CSS frameworks like Bootstrap having widgets that contain 3 or 4 parts. With deep trees and each level adding new parts I can see the outer components might have an absurd number of things to forward.

::theme gives you an escape hatch for the cases where you have global-esque styles, such as when you use a CSS framework. Being able to use ::theme(bootstrap-form) and define the styles only once will be a big advantage.

@rniwa
Copy link
Collaborator

rniwa commented Feb 13, 2017

I'm not discounting or denying the importance of use cases for @apply and ::theme here but I'd like to keep this thread's discussion focused on ::part. I think we need a separate thread for discussing the merits of @apply and ::theme.

@jouni
Copy link

jouni commented Mar 10, 2017

One thing I’ve been wondering with the new “custom shadow parts” proposal is how do we expose the host element for theming by default? Can we add a part attribute for the host as well (by the element itself, automatically)? Or is it always necessary to forward the host element as a new shadow part?

As a simple use case, if I have a button component I want to provide themes for, which are not bundled with the component, and I’m not the author of the button component. The component has only the host element, no extra elements/parts in shadow DOM.

So, I would like to offer users an additional stylesheet they can load, which would give a new default look for the button, but also offer some extra styles/variations for it, like “small”, “large” and “primary”.

<link rel="stylesheet" href="theme-for-nice-button.html">

<!-- This would now look different than the <nice-button> component looks without the theme -->
<nice-button>Button</button>

<!-- I would like to offer these kind of additional styles as well -->
<nice-button class="small primary">Small Primary Button</nice-button>

Could the <nice-button> component automatically add a new part value for itself, so the generated DOM would look like this:

<nice-button part="nice-button">Button</button>
<nice-button part="nice-button" class="small primary">Small Primary Button</nice-button>

And I could then write the following CSS in the theme I provide:

html::theme(nice-button) {
  /* My new default styles for nice-button */
}

html::theme(nice-button).primary {
  /* Additional styles for the primary button */
}

Am I completely off? Has this use case been considered in the “custom shadow parts” proposal?

@rniwa
Copy link
Collaborator

rniwa commented Mar 14, 2017

No, that's a use case for @apply and ::theme, which should really be discussed in a separate thread.

@jouni
Copy link

jouni commented Mar 14, 2017

Alright. Do we have that thread already somewhere?

@tomalec
Copy link
Contributor

tomalec commented Nov 28, 2017

It was discussed at TPAC2017, the minutes are at https://www.w3.org/2017/11/10-webplat-minutes.html#item03

@annevk
Copy link
Collaborator

annevk commented Feb 19, 2018

It seems this specification is still hosted in @tabatkins's private GitHub, despite there being feedback tracked here as well: https://github.com/w3c/csswg-drafts/issues?q=is%3Aissue+is%3Aopen+css-shadow-parts.

Will the CSS WG adopt this soonish?

Can we close this issue as this is now mostly a CSS WG matter?

@rniwa
Copy link
Collaborator

rniwa commented Feb 19, 2018

Again, we're strongly in favor of having this feature. We have a strong interest in implementing this feature in WebKit as well.

@trusktr
Copy link

trusktr commented Feb 19, 2018

Not sure if this is the right place, but as a custom elements author, I'd like a way to define toggle :hover for custom elements.

EDIT: made a new issue for it: #738

@tabatkins
Copy link

It seems this specification is still hosted in @tabatkins's private GitHub, despite there being feedback tracked here as well: https://github.com/w3c/csswg-drafts/issues?q=is%3Aissue+is%3Aopen+css-shadow-parts.

No, I just haven't marked my personal copy as being obsolete. The spec is at https://drafts.csswg.org/css-shadow-parts/ and is officially tracked by the CSSWG.

@annevk
Copy link
Collaborator

annevk commented Feb 21, 2018

Let's close this then. I have updated https://github.com/w3c/webcomponents/blob/gh-pages/README.md to point out that proposal so we still have a centralized place to look at for where the various web component bits ended up.

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