WICG / webcomponents

Web Components specifications
Other
4.4k stars 376 forks source link

custom pseudo elements like ::before ::after, but using custom elements #983

Open trusktr opened 1 year ago

trusktr commented 1 year ago

(This is not a duplicate of the old custom pseudo elements proposal, which has been replaced with ::part)

It might be useful to be able to define custom pseudo elements that can be custom elements.

f.e.

.foo::custom-element-pseudo('my-element') { /* <my-element> is a custom element */ 
  /* ... style the custom element that is "injected" into the DOM similar to ::before or ::after. */
}

The idea here is that we want to be able to use custom elements in a way similar to ::before and ::after to "inject" custom content into an app purely with CSS, and we want this content to be that which is defined by a custom element, plus additional styling.


More advanced example:

<!-- With HTML composition (WET): -->
<lume-mesh position="1 2 3">
    <phong-material color="red"></phong-material>
    <sphere-geometry wireframe></sphere-geometry>

    <lume-mesh position="1 2 3">
        <!-- not DRY (WET): repeat the same elements to give the sub-mesh the same look and shape -->
        <phong-material color="red"></phong-material>
        <sphere-geometry wireframe></sphere-geometry>
    </lume-mesh>
</lume-mesh>

<!-- With CSS custom pseudo elements (DRY): -->
<lume-mesh position="1 2 3">
    <!-- children in the scene graph -->
    <lume-mesh position="1 2 3"></lume-mesh>
</lume-mesh>
<style>
    /* style both meshes with the same geometry and material */
    lume-mesh::custom-element-pseudo('sphere-geometry') {
        wireframe: enable;
    }
    lume-mesh::custom-element-pseudo('phong-material') {
        color: red;
    }
</style>

Maybe it should be more generic, like ::element-pseudo('any'), including built-ins?

What do you think of this idea?


problem that it solves

Besides making the above example more DRY (plus the amazing fact that CSS could enable so much), another way to implement the mesh styling is with element-behaviors, as I have currently done with LUME 3D HTML:

<lume-mesh position="1 2 3" has="phong-material sphere-geometry" color="red" wireframe>
    <lume-mesh position="1 2 3" has="phong-material sphere-geometry" color="red" wireframe>
    </lume-mesh>
</lume-mesh>

The advantage of this is I can easily map CSS Custom Properties like --material: phong; --color: red; --wireframe: enable to the JS properties (that back the HTML attributes) very much like SVG, because CSS Custom Properties are accessible from JavaScript (for example, the simple way to detect their changes would be in an animation frame loop, and without looping we need more-complicated DOM observation).

However, when everything is in attributes, this is a lot less composable. Having child elements like <sphere-geometry> is highly composable because the elements can be in the shadow DOM of higher-level custom elements. F.e.:

<lume-mesh>
  <my-mesh-style></my-mesh-style>
</lume-mesh>

where <my-mesh-style> is a custom element whose ShadowRoot contains:

    <phong-material color="red"></phong-material>
    <sphere-geometry wireframe></sphere-geometry>

This ability to compose is very useful. However, a CSS implementation with CSS Custom Properties can no longer simply map properties to all meshes, so the composition version of my example is not so CSS implementation friendly, because selectors targeting meshes are not targeting the children that have the styling features.

I think it might be possible to hack this with CSS Custom Properties, but maybe it would be less ideal than an official syntax. For example:

lume-mesh {
  --pseudo-sphere-geometry: ;
  --geometry-wireframe: enable;

  --pseudo-phong-material: ;
  --material-color: red;
}

The implementation that reads these custom CSS properties can map those to essentially the same child elements inside the ShadowDOM of the targetted <lume-mesh> elements, or something.

official syntax

But having an official syntax for it would feel nicer. Elements would perhaps have a new property like .pseudoElements which is similar to .children, but lists only pseudo elements added via CSS.

Then, an implementation of <lume-mesh> only needs to look in one list or the other, but otherwise the logic that wires the style of the mesh would be essentially the same.

To make this cleaner for the end user, with a polyfill of the desired syntax, would greatly increase the complexity of an implementation, compared to reading CSS custom props.

trusktr commented 1 year ago

another way to make code DRY

Maybe declarative custom elements would solve this.

Upsides:

Downsides:

sashafirsov commented 1 year ago

@trusktr ,

Maybe declarative custom elements would solve this.

You must be joking. There is nothing declarative about proposal you mentioned. Alternative would be pure declarative syntax.

Exposing the DCE to CSS as ::custom-element-pseudo('my-element') is definitely a great idea. The polyfill should be implementable but aware of custom elements registry scopes. I.e. have more sense to be a part of scoping.

rniwa commented 1 year ago

How is this different from exposing custom elements using ::part?

trusktr commented 1 year ago

How is this different from exposing custom elements using ::part?

This is absolutely nothing like ::part.

I even mentioned on the very first line this is not a duplicate of that proposal:

(This is not a duplicate of the old custom pseudo elements proposal, which has been replaced with :part)

I would greatly appreciate some due diligence! πŸ˜ƒπŸ™


This is like ::before/::after except it injects an element of choice (f.e. inject a <whatever-element-you-want>).

OmarCastro commented 1 year ago

While the idea is good, I agree with @rniwa, it is not a duplicate of the proposal, but it solves the same problem, by exposing the custom element using ::part

In your <my-mesh-style> example when setting element with part

<phong-material part="phong"></phong-material>
<sphere-geometry part="sphere"></sphere-geometry>

you can apply css directly with

my-mesh-style::part(phong) {
  --material-color: red;
}

my-mesh-style::part(phong) {
  --wireframe: enable;
}

you can then replace with more specific styles

Another thing about pseudo elements ::before/::after, they are additional visual only elements, they have no logic, meaning they are not interactable (afaik, any form of interaction is propagated to the element containing the pseudo one), and I feel that custom element we will want some logic that we expect to happen when we use it (e.g. user interacton), however that becomes something out of scope of style sheets.

Also, there is a positioning issue, ::before is a pseudo element positioned before the element and ::after to be after it, how would we handle it with your proposal?

trusktr commented 1 year ago

but it solves the same problem, by exposing the custom element using ::part

That's a different scenario than I've outlined. I'm saying is, suppose we write this HTML (nothing more):

<mesh-3d></mesh-3d>

Now, we need to style it (in CSS) as if we had written this markup:

<mesh-3d>
  <phong-material ...></phong-material>
  <box-geometry ...></box-geometry>
</mesh-3d>

Or as if we had written this:

<mesh-3d>
  <physical-material ...></physical-material>
  <sphere-geometry ...></sphere-geometry>
</mesh-3d>

without actually having changed the original <mesh-3d></mesh-3d> HTML.

This is not about which elements are known to be inside a ShadowRoot (::part).

See, totally different than ::part! And similar to ::before/::after.

I feel that custom element we will want some logic that we expect to happen when we use it (e.g. user interacton),

Yeah, true. Maybe it would be doable? Or maybe interaction would be disabled (f.e. dispatchEvent would throw if called on such an element), otherwise built in events won't be triggered by it. Maybe interactions do only happen on the DOM host. Maybe dispatchEvent dispatches from the DOM host. Perhaps we can make rules.

there is a positioning issue, ::before is a pseudo element positioned before the element and ::after to be after it, how would we handle it with your proposal?

perhaps it can be solved like

mesh-3d::element-before(physical-material) {
  --normal-map: url(/path/to/map.jpg);
}
mesh-3d::element-after(box-geometry) {
  --wireframe: enable;
}

For this to be useful (f.e. custom rendering to canvas), we need to access these pseudos. F.e. something like

const mesh = document.querySelector('mesh-3d');
const material = mesh.pseudosBefore[0]
const normalMapUrl = getComputedStyle(material).getPropertyValue('--normal-map')

fetch(normalMapUrl).then(() => ... apply to rendering objects ...)
sashafirsov commented 1 year ago

I would extend this proposal with Declarative Custom Element loaded via URL

.foo::custom-element-pseudo(url('path-to-dce')) { 
  /* custom element is anonymous , i.e. does not have associated dedicated tag but its loading location  
     works as identifier. I.e. any tag would match when DCE is originated from URL
  */ 
  /* ... see original proposl ^^ */
}
sashafirsov commented 1 year ago

@trusktr , Would the use in a content in ::before be alternative to your proposal?

q::before {
  content: url(path-to-template);
  color: blue;
}

Here is a template with or without DCE would be used as injected DOM.