Support Custom Pseudo-elements #300

Open
hayatoito opened this Issue Sep 1, 2015 · 45 comments

Projects

None yet
@hayatoito
Member

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 hayatoito added the shadow-dom label Sep 1, 2015
@hayatoito
Member

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
Contributor
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
Member

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
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
Member

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

@morewry
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
Member

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
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
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
Contributor
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
Contributor
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
Member

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
Member

@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
Contributor
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
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
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
Contributor
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
Member

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
Member

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
Contributor
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
Member

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.

@hayatoito hayatoito added the v2 label Mar 17, 2016
@Zambonifofex

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
andyearnshaw commented Jun 10, 2016 edited

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).

@Zambonifofex

@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>
@Zambonifofex

By the way, I’d like to state that my opinions changed about this: I’m starting to like @apply.

I think that it’d be really neat if browsers allowed us to style their vendor‐specific “pseudo‐elements” using a similar mechanism:

input
{
    -webkit-inner-spin-button:
    {
        /* ... */
    };
}

Please note how using a single dash avoids collision with user‐defined variables.

@trusktr
trusktr commented Jul 13, 2016

@tabatkins Can you give (or link to) an example of @apply being used to expose stylable parts of a shadow tree?

@hayatoito
Member

cc: @dominiccooney , @shans
This might be an issue you are interested in.

I think this issue is a worth triaging for v2. If someone has an updated opinion or idea, please let us know that.

@seaneking

Just some quick input as a user - another case where I've found @apply currently falls apart is with composition. Eg: I have an element that exposes a mixin, I apply a 'base theme' to that element in my app, I want to add extra styles on top of that base theme in a specific instance of the element. Right now if I @apply another mixin on the element it clobbers the base theme.

A workaround for explicit inheritance for @apply was discussed at tabatkins/specs#49, though the proposed syntax is perhaps a little unintuitive for users, whereas composition with parts (should?) 'just work'.

@tabatkins
Member

@seaneking Can you elaborate on the failure case you're talking about more, perhaps with a code example? I don't understand how you're getting a clobbering.

@seaneking
seaneking commented Oct 14, 2016 edited

@tabatkins perhaps I've misunderstood the spec (re: cascading in https://tabatkins.github.io/specs/css-apply-rule/#defining), but wouldn't the following result in overwritten styles?

Assuming an element x-foo with local DOM

<template>
  <style>
    .inner {
      @apply --my-mixin;
    }
  </style>
  <div class="inner"></div>
</template>

And I use it on my page

<x-foo class="styled"></x-foo>

Then style all x-foo's on my page with some base styles, and some additional styles for this particular one

x-foo {
  --my-mixin: {
    background: blue;
  };
}

/* Can't inherit background blue */
x-foo.styled {
  --my-mixin: {
    border: 1px solid red;
  };
}

--my-mixin doesn't cascade, so I can't compose. I think this is the correct behaviour, it just makes them hard to reason about in custom elements.

Specific eg: I'm putting together a collection of UI elements (SimpleElements), and applying base styles for them throughout our app (using mixins). But if I touch one of them again the base is overwritten and the whole notion becomes quite redundant.

TL;DR I think (again, as a user) that mixins are extremely useful for doing what they say on the tin - giving you bags of styles to mixin to multiple elements. But feels like the function of exposing parts of custom elements would be better served by custom pseudo elements or similar.

@tabatkins
Member

Ah, yes, custom properties cascade like every other property does, with one value winning per element; the fact that one of them might contain a declaration block intended to be sent to an @apply doesn't change things.

I have a proposal for some cascade controls that would let a property "merge" with its earlier-in-the-cascade variants; if the custom property was appropriately typed as carrying a declaration list (we don't have a type for that in Custom Properties & Values yet, but we'll add it when @apply is officially accepted), then --my-mixin+: {...}; would work. (It would be considered a list-valued property, I guess, just with a different delimiter than normal.)

@seaneking

Ah okay, I guess that would solve that issue. Still, you'd now have nesting and ampersand operators to handle pseudo classes, and 'merge' combinators to handle cascading, when neither really seem in the scope of a mixin's primary purpose (mixing in a bag of styles to multiple elements), and the alternative is just to expose a paradigm that developers are already familiar with (pseudo elements) which 'just works' for all of these cases. And FWIW, I personally find ::part a less radical introduction than nesting in CSS, especially since it's become a bit of an antipattern in preprocessors.

I can't really comment on future-of-the-platform stuff, but even from a developer ergonomics standpoint, just seems we're overloading mixins with lots of new things for the sake of not introducing a new thing.

@jouni
jouni commented Jan 8, 2017

@seaneking I’m not entirely sure, but I think you can solve your use case in the following way (nesting @apply):

x-foo {
  --my-mixin: {
    background: blue;
    @apply --my-mixin-extras;
  };
}

x-foo.styled {
  --my-mixin-extras: {
    border: 1px solid red;
  };
}

It definitely feels like a workaround (and is it even allowed by the proposed spec?), as you need to introduce another name for the mixin after each “level”.

@seaneking

@jouni then you're tightly coupling the two sets of styles, which isn't really tenable in a lot of use cases, and in others is just asking for unmaintainable code.

I think the 'cascade controls' proposed by @tabatkins would be a better solution for, well, controlling cascade. My big concern is adding all these band-aids to mixins for a job they (I'm assuming) weren't originally designed to do, instead of using a paradigm that has already been proven to work for all of these use cases (pseudo elements).

@rniwa
Contributor
rniwa commented Jan 9, 2017

Well, we're adding custom pseudo elements. That's what this issue is about.

@jouni
jouni commented Jan 9, 2017

@seaneking I agree. It was just something I was considering as a workaround for our theming use case in vaadin-core-elements and wanted to hear how others feel about it.


@rniwa How likely/official is that (getting custom pseudo elements added to the spec)?


I feel like I want to contribute some more thoughts about this proposal, and why I also think custom pseudo elements are needed and @apply is not sufficient by itself.

I’ve come to the same conclusion as Philip, that @apply has shortcomings together with component/element states. The use case I have trouble currently is a data table row (in vaadin-grid to be more exact), and how to provide styling hooks for that.

<data-table>
  #shadow
    ...
    <div class="row selected">
      <!-- Light DOM -->
      <span>Cell data</span>
    </div>
    ...
</data-table>

The rows can have different states, such as selected, expanded, hover, etc.

Now, with @apply, we get something like this:

.row {
  @apply --data-table-row;
}

.row:hover {
  @apply --data-table-row-hover;
}

.row.selected {
  @apply --data-table-row-selected;
}

To me, those mixin names have a bad code smell. It feels like they are just exposing the selector, but with a less semantic syntax. I’ve considered if we should use BEM like syntax for mixin names (--data-table__row--selected), but that feels like an ugly workaround as well, exactly what we should be fixing with Web Components (not having to come up with naming conventions like this).

Also, with @apply, the component author dictates the specificity of those “selectors”. The user can’t style a selected row which is hovered differently from regular rows that are hovered. Also, if I want a hovered row background to be #eee, and the selected row background to be #e6e6e6, I no longer have any hover style for my selected rows, and I have no way to reverse that specificity (have row hover override selected row styles). That would require another mixin (--data-table-row-selected-hover), just like Philip points out in his proposal as the main problem, but it would still have the specificity problem.

User styles:

data-table {
  /* The order of these mixin declarations do not affect the specificity at all */
  
  --data-table-row-hover: {
    background-color: #eee;
  };
  
  --data-table-row-selected: {
    background-color: #e6e6e6;
  };
  
  /* Additionally if needed */
  --data-table-row-selected-hover: {
    background-color: #e1e1e1;
  };
}

@apply feels more like the component reaching to the outside to grab something the author has good expectations of (high-level global font/color declarations for example), whereas custom pseudo elements feel like the component offering a way for users to reach inside the component to provide customized styles when the component itself can’t really be expected to know how it should look like. @apply feels like it’s inverting too much control.

As a comparison, we would do the following with custom pseudo elements:

<data-table>
  #shadow
    ...
    <div pseudo="row" class="selected">
      <!-- Light DOM -->
      <span>Cell data</span>
    </div>
    ...
</data-table>

User styles:

/* The order of these declarations affect specificity as expected */
data-table::row(:hover) {
  background-color: #eee;
}

data-table::row(.selected) {
  background-color: #e6e6e6;
}

/* Additionally */
data-table::row(.selected:hover) {
  background-color: #e1e1e1;
}

With custom pseudo elements, we also gain the ability to style the light DOM nodes depending on the pseudo elements and their state, which is impossible with @apply (without nesting support at least).

User styles:

data-table::row span {
  color: #000;
}

data-table::row(.selected) {
  background-color: blue;
}

/* This is impossible with mixins */
data-table::row(.selected) span {
  color: white;
}

The same with @apply/nesting:

data-table {
  --data-table-row: {
    & span {
      color: #000;
    }

    &.selected {
      background-color: blue;
    }

    &.selected span {
      background-color: white;
    }
  };
  
}

Not sure what other implications (performance?) this kind of “light DOM element targeting inside pseudo elements” would have.

This data table use case might not be the best one, as you could argue those <div class="row"> elements should be in the light DOM, authored by the user (for example as <data-table-row> custom elements). But as it is currently, those elements are generated at runtime, and the light DOM contents are generated based on user provided templates (basically how <iron-list> works). So it’s a use case at least for us.


I was actually unaware that nesting might be added to the spec and that it could work together with mixins. That might be a solution to these use cases as well. Personally, I like the pseudo element syntax more, though.


Lastly, I wanted to try and provide “answers” to the issues Tab pointed out with custom pseudo elements:

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?

As should be obvious from my use case above, I think shadow-pseudos should be more like classes, and allow selecting multiple elements.

If you can have multiple, how do we select a single one to style? Does the component author have to allow this?

If you want to select a single one, I think it should work like regular light DOM, i.e. data-table::row:first-child, and the author allows this by adding the pseudo attribute to all the elements that they want to get selected.

Can a single element expose multiple pseudo names for itself?

Can’t think of a use case for this.

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?

Not sure how necessary it would be to allow nested pseudo-elements, but I think I would expect date-picker::date-cell.selected::before to work.

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"?

Without spending too much time thinking about this, I don’t feel a strong need to expose pseudo-elements outside another shadow boundary. I guess @apply could cover those cases.

For example:

<x-foo>
  #shadow
    <style>
      date-picker::date-cell {
        @apply --x-foo-date-cell;
      }
    </style>
    <date-picker>
      #shadow
        <div pseudo="date-cell"></div>
    </date-picker>
</x-foo>

But I admit, this is a tough issue, and Philip’s proposal doesn’t cover this, apart from proposing that custom-pseudos should always be prefixed with the custom element name.

I also admit I have no idea what kind of performance problems custom pseudo elements could cause if they can cause similar problems as /deep/.

@hayatoito
Member
hayatoito commented Jan 10, 2017 edited

I think we, browser vendors, roughly agreed on adding custom pseudo elements last year's TPAC, however, no one is working on a concrete proposal actively, as of now. I guess that is simply due to our bandwidth problem. :(

@jouni
jouni commented Jan 10, 2017

@hayatoito That’s understandable (bandwidth problems). But great to hear there’s at least a consensus on custom pseudo elements.

Is there any way the community can help with the bandwidth? I mean, can some parts of the proposal work be delegated outside the working group?

@phistuck

@jouni - I think you can always submit a pull request with the changed/added specification sections and someone from the working group will review it.

@BronislavKlucka
@othermaciej
Member

The main intent for shadow-pseudos (or "part selectors" as I like to call them) is actually to represent parts of a compound custom element, not states. This makes them more like pseudo-elements than pseudo-classes. In fact, the analogues for styling of UA built-in controls are pseudo-elements.

To meet the use case of styling specific parts of custom elements (like only the text field of a combo box, or only the button, or only the menu), it needs to be possible for more than one part to match the same part selector. The reason is that sometimes a control has more than one of logically the same part repeated. Sometimes the number of repeats of the part may be variable. For example, an image carousel custom element may want to let you style the images (to add a border, a filter effect, etc). It would be much simpler to have a single "content-image" pseudo, than to have a separate indexed one for each image.

I'm not sure if the opposite is necessary - one element in the shadow DOM corresponding to more than one part. Offhand I can't think of a use case that requires this.

@rniwa
Contributor
rniwa commented Jan 17, 2017

One element in the shadow DOM corresponding to more than one part.

Here's one use case. If you had a carousel, you might want to have one selector to match any image, and another one to match the currently selected/shown image.

@jouni
jouni commented Jan 18, 2017

One element in the shadow DOM corresponding to more than one part.

Here's one use case. If you had a carousel, you might want to have one selector to match any image, and another one to match the currently selected/shown image.

That sounds like a state, rather than a logical part. So I would like to be able to target shadow-pseudos in different states, but they would still just have one matching pseudo selector – something like ::shadow-pseudo(.state).

@rniwa
Contributor
rniwa commented Jan 18, 2017

One element in the shadow DOM corresponding to more than one part.

Here's one use case. If you had a carousel, you might want to have one selector to match any image, and another one to match the currently selected/shown image.

That sounds like a state, rather than a logical part. So I would like to be able to target shadow-pseudos in different states, but they would still just have one matching pseudo selector – something like ::shadow-pseudo(.state).

It's indeed a state. However, wanting to style an element with a particular state is not mutually exclusive with wanting to style an element of a particular part. In fact, if you look at a typical website's CSS, most of CSS selectors are concerned about finding parts of a page to style, not necessary of a particular state. I'd imagine that's why @othermaciej said "the main intent", not "the intent", or "the only intent".

@othermaciej
Member
othermaciej commented Jan 18, 2017 edited

Perhaps there is value in styling both parts and states, and thus if we want to be sure CSS-ishly correct, there may need to be both shadow pseudo-elements and shadow pseudo-classes (for states). I would suggest they could be named ::part() and :state() (different number of colons intentional). You'd target the active image with ::part(image):state(active). Having shadow pseudo-classes would also allow defining styling for custom states of the whole custom element, if pre-defined pseudo-classes like :active and :hover are not enough.

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