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

Consider using Shadow Parts as an alternative to pseudo-elements for structure and user styling #190

Open
bgrins opened this issue Sep 2, 2022 · 12 comments

Comments

@bgrins
Copy link

bgrins commented Sep 2, 2022

I'd like to consider Shadow Parts as an alternative to using nested pseudo-element structure for user styling. Maybe this has already been discussed and dismissed, but I wanted to raise this coming out of mozilla/standards-positions#677.

I'm not sure it's ideal to expose the deep pseudo-element tree to developers - both ergonimcally, and in the case where a UA may want to change the internal representation of the various parts i.e. if future iterations introduce new features which would benefit from a change in the tree.

So instead of having something like

::page-transition
├─ ::page-transition-container(root)
│  └─ ::page-transition-image-wrapper(root)
│     ├─ ::page-transition-outgoing-image(root)
│     └─ ::page-transition-incoming-image(root)
│...

with

::page-transition-container(root)::image-wrapper::outgoing-image {
  /* … */
}

Could there be something like:

<div part="page-transition">
├─ <div part="page-transition-root-container">
│  └─ <div part="page-transition-root-image-wrapper">
│     ├─ <div part="page-transition-root-outgoing-image">
|     └─ <div part="page-transition-root-incoming-image">
|...

with

:root::part(page-transition-root-outgoing-image) {
  /* … */
}

Some of the potential challenges with this approach (at least the ones I've thought of) are:

  • What if a user attached their own Shadow Root to the root element? That's not allowed on html elements, but what would happen if a developer replaced the documentElement with an element that does support them? Perhaps the feature isn't supported in that case.
  • I don't believe UA shadow parts have been exposed to content CSS before so this would have to be an exception
@jakearchibald
Copy link
Collaborator

I'm not sure it's ideal to expose the deep pseudo-element tree to developers - both ergonimcally, and in the case where a UA may want to change the internal representation of the various parts i.e. if future iterations introduce new features which would benefit from a change in the tree.

Can you give a bit more detail on this? As in, why would changing the pseudo-tree be bad, but changing the shadow-tree be fine?

The benefit of the shadow tree is developers could add their own elements for the transition, and use familiar APIs like el.getBoundingClientRect(). But another challenge is handling cases where the developer takes elements out of the shadow root and puts them in the main document.

@khushalsagar
Copy link
Collaborator

Can you give a bit more detail on this? As in, why would changing the pseudo-tree be bad, but changing the shadow-tree be fine?

Yeah. Even if the tree structure isn't directly expressed in the selector the CSS to customize the transition set by developers will assume a particular tree structure. It's part of the API contract either way.

The benefit of the shadow tree is developers could add their own elements for the transition, and use familiar APIs like el.getBoundingClientRect()

I'm hoping that we can add the subset of the Element API, that makes sense to expose, via CSSPseudoElement as we learn developer use-cases.

@bgrins
Copy link
Author

bgrins commented Sep 7, 2022

Can you give a bit more detail on this? As in, why would changing the pseudo-tree be bad, but changing the shadow-tree be fine?

I was thinking in the sense that CSS would be hardcoding the nesting for pseudo-elements, such that if a UA was to change some internal representation (like wrapping everything in another container, adding a new element, etc) it would be observable. As @khushalsagar points out, maybe it's optimistic to think this could happen in either case since authors will write CSS based on the structure.

Regardless, it may be subjective but I find the Shadow Parts syntax more ergonomic, and that the shadow DOM more intuitively expresses what the feature is doing to a web developer.

The benefit of the shadow tree is developers could add their own elements for the transition, and use familiar APIs like el.getBoundingClientRect(). But another challenge is handling cases where the developer takes elements out of the shadow root and puts them in the main document.

@emilio do you have thoughts on this? I was actually imagining that the UA DOM wouldn't be exposed to scripting to avoid the challenge here, but if access to DOM APIs is a requirement then I actually don't know if it's better to add a subset of those APIs to pseudo-elements, or restrict a subset of the APIs from UA shadow content. You probably have more context and an opinion here.

@khushalsagar
Copy link
Collaborator

I find the Shadow Parts syntax more ergonomic, and that the shadow DOM more intuitively expresses what the feature is doing to a web developer.

Hmmm, the API feels similar to me for both once we have the descendent selector for pseudo-elements but curious to hear thoughts from other folks too. @tabatkins for CSS stuff.

Pseudo-elements

root:: :>> outgoing-image(root) { 
  ...
}
  document.documentElement.animate(
    {
      ...
    },
    {
      // Specify which pseudo-element to animate
      pseudoElement: ":: :>> outgoing-image(root)",
    }
  );

Shadow DOM

root::part(page-transition-root-outgoing-image) {
  ...
}
  document.documentElement.animate(
    {
      ...
    },
    {
      // Specify which pseudo-element to animate
      pseudoElement: "::part(page-transition-root-outgoing-image)",
    }
  );

I'll also look into any other issues with attaching a shadow DOM to the root element. I'll admit that while implementing this I only tried the shadow DOM under pseudo-element (which ran into issues with multiple feature doing flat tree traversal) and a tree of pseudo-elements.

if access to DOM APIs is a requirement

The WA-API is definitely a requirement but that can be exposed via the part selector for the shadow DOM case too. @jakearchibald built a bunch of interesting demos which benefited from access to APIs like getBoundingClientRect and inline style. But each case we discussed has been a capability we could see being exposed for all pseudo-elements and one of the reasons they seemed like the right conceptual fit.

@tabatkins
Copy link

maybe it's optimistic to think this could happen in either case since authors will write CSS based on the structure.

Note in particular that the nested structure isn't just for convenience, but is in fact required in order to get blending between before/after states working correctly. So the structure is even depended on by the current spec!


More generally, I'm against this change. The pseudos aren't, actually, "on" any element at all; we attach them to the root just because syntactically they need to be on something, but in layout they're moved into the top layer and thus paint completely independently anyway. On the other hand, putting a UA shadow root on an element is meaningful and observable; it can't co-exist with an author shadow root. Putting a shadow root on html might be rare, but I wouldn't say it's out of the question, and having that incidentally prevent the page from using SET would be unfortunate and confusing.

@bgrins
Copy link
Author

bgrins commented Sep 7, 2022

On the other hand, putting a UA shadow root on an element is meaningful and observable; it can't co-exist with an author shadow root. Putting a shadow root on html might be rare, but I wouldn't say it's out of the question, and having that incidentally prevent the page from using SET would be unfortunate and confusing.

My understanding is that it's not possible to attach a shadow root on the html element from https://dom.spec.whatwg.org/#dom-element-attachshadow:

If this’s local name is not one of the following:

    a valid custom element name
    "article", "aside", "blockquote", "body", "div", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "main", "nav", "p", "section", or "span" 

then throw a "NotSupportedError" DOMException.

Regardless, I think there have been some really good points raised so far here and it helps me better understand the decision to use pseudo-elements. I'd like to see if Emilio or others have thoughts but I don't feel strongly enough to keep this open myself.

@emilio
Copy link

emilio commented Sep 7, 2022

Yeah, author shadow dom on <html> is invalid, it just throws.

And exposing them via shadow parts doesn't mean that they are exposed to script, so I'm not sure what that is about, they wouldn't be exposed to script just like the current pseudos aren't.

In general I don't know there'd be any particular behavior difference here other than in the case where the document element is not an <html> element (do we care about that? I guess not). And I'd personally rather prefer using the existing features the platform provides rather than providing a rather complex pseudo-element structure that will only be used by this single feature.

@tabatkins
Copy link

Ah, I'd forgotten that there was a specific restriction against html shadow roots. That avoids one issue, sure. (And yeah, it being a tagname restriction rather than a restriction against the "root element" seems weird; that should maybe be fixed in HTML.)

And exposing them via shadow parts doesn't mean that they are exposed to script, so I'm not sure what that is about, they wouldn't be exposed to script just like the current pseudos aren't.

The point was about exposing the existence of UA-defined shadow parts, not about exposing the objects via script. The former hasn't been done yet.

And I'd personally rather prefer using the existing features the platform provides rather than providing a rather complex pseudo-element structure that will only be used by this single feature.

Conversely, we haven't exposed any UA-defined parts at all in the platform yet, whereas pseudo-elements are common. And, while nested pseudo structures haven't been defined in a spec yet, they definitely exist in browser-specific forms, like the scrollbar pseudos. So I think there's just as strong, if not stronger, of an argument for pseudos being the existing platform feature we're relying on. ^_^

More generally, @emilio, do have specific reasons to not expose these as pseudo-elements? Does this add some difficulties that I'm not aware of?

@emilio
Copy link

emilio commented Sep 8, 2022

More generally, @emilio, do have specific reasons to not expose these as pseudo-elements? Does this add some difficulties that I'm not aware of?

Not difficulties per se, but a bunch of special-case code around the style engine. This is true of ~every engine afaict. Pseudo-elements generally need special code, in parsing/serialization/matching, and pseudo-element generation, see all the usages of the page transitions pseudos in Chromium for example.

Introducing a bunch of new pseudos and more complex tree structures for a feature that is already complex enough on its own seems pretty overkill to me, but I dunno, might not be the hill to die on. It just feels really over-complicated when the platform already provides a way of doing that that we could use for this rather special case...

@khushalsagar
Copy link
Collaborator

@emilio the API currently exposed for these pseudo-elements is the CSS selector and the few script APIs which are already exposed for pseudo-elements. Without the descendant pseudo-element selector (yet to be spec'd and implemented), that would look something like the following:

html::page-transition::container(foo) {
  ...
}
document.documentElement.animate(..., { pseudoElement: "::page-transition::container(foo)" });

Do you think an API surface like this could allow both a shadow DOM and pseudo-element implementation? We need to have a selector syntax to target these elements irrespective of the backing implementation. The shadow DOM version implies that this syntax could be the part selector.

But if the additional parsing code needed for this chaining syntax is reasonable to support with a shadow DOM too then we can go with pseudo-elements for now. This will help in deferring a decision on the exact backing implementation before committing to additional APIs which are tightly coupled to either of the 2. Since one of your concerns with the pseudo-element approach is the special code in parsing/matching, I wasn't sure if this idea helps.

@tabatkins
Copy link

No, ::part() is unfortunately not equivalent to nested pseudos, as it very specifically and purposely does not expose nesting details. So in particular, the use-case for detecting whether a transition has an incoming and/or outgoing image won't work without additional effort twiddling part names to reveal that info.

@khushalsagar
Copy link
Collaborator

Sorry if my comment was confusing, I wasn't advocating for using ::part(). But the idea of exposing the pseudo-element based CSS syntax which an engine could choose to back with shadow DOM or pseudo-elements. There are use-cases where we've added bespoke pseudo-element selectors for elements in UA shadow DOM (like ::placeholder). But the fact that we have nested pseudo-elements here makes it likely that pseudo-elements end up being the easier backing implementation.

You brought up another good point that there are use-cases where developers want to conditionally apply CSS based on the tree structure, that was brought up here. This can work with the nested pseudo-element syntax combined with has but I don't see it working with part. So if we have use-cases like this which need a pseudo-element based syntax, is it simpler to back it with shadow DOM vs pseudo-elements?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants