Skip to content

[css-pseudo] Can we make pseudo-elements first-class citizens in the DOM? #11559

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

Open
bramus opened this issue Jan 22, 2025 · 5 comments
Open
Labels
css-pseudo-4 Current Work

Comments

@bramus
Copy link
Contributor

bramus commented Jan 22, 2025

Fun with Pseudos

In light of View Transitions I’ve been playing a lot with pseudo-elements trying to do all sorts of things with them from within JavaScript:

  • Access them
  • Get info about their position+size
  • Animate them
  • Get their animations
  • Access their styles

Accessing the pseudos themselves is in theory possible through Element.pseudo() but in practice it has no browser support1.

Some of the other features I listed above are made possible in some APIs through means of the pseudoElement option. When working with these it becomes quickly pretty clear that this pseudoElement is an afterthought that was introduced in order to get it to work quickly, instead of fully adopting the existence of CSSPseudoElement. See #4301 for example that chose to add the pseudoElement option to Element.animate instead of embracing CSSPseudoElement.

Since then other APIs have also been monkey-patched to accept a pseudoElement option:

So as for the things I want to do with pseudos this is the current state of affairs:

  • Access them: Theoretically using pseudo() but that has not shipped anywhere.
  • Get info about their position+size: Not directly possible (you could anchor a real Element to it and then do getBCR on that Element).
  • Animate them: Only if they already have animation attached to them that you can hijack
  • Get their animations: Theoretically using Element.getAnimations() but that is not specced yet. You can sniff ’m manually, though.
  • Access their styles: getComputedStyle() with pseudoElt param.

With more and more features leaning on pseudo-elements (select::picker, ::details-content, carousel pseudos, customizable select, etc.) this problem will become more visible to authors.

Making pseudo-elements first-class citizens

Instead of monkey-patching more APIs to accept a pseudoElement, I believe it would be nicer if pseudo-elements becamse first-class citizens of the DOM. All the pieces of the puzzle are there, it’s a matter of putting them together:

Just a starting point

Most likely I’m overlooking a bunch of things and I’m definitely lacking some nuances to some of the many pseudos that would allow/disallow this – so I’m very much looking forward to input on this one.

Footnotes

  1. Thankfully I didn’t really need the pseudos directly in my experiments. Because I only needed the animations+effects applied to them, I was able to sniff them out through document.getAnimations()

@bramus bramus added the css-pseudo-4 Current Work label Jan 22, 2025
@Loirooriol
Copy link
Contributor

I don't think they can completely become first-class citizens in the DOM. Because in events, the target isn't a pseudo-element, and allowing it to be a CSSPseudoElement seems most likely not web compatible.

Get info about their position+size

In some cases you can use getComputedStyle to get the height and width.

I don't think getBoundingClientRect() makes much sense for non-tree-abiding pseudo-elements like ::selection.

Animate them: Only if they already have animation attached to them that you can hijack

You don't need to hijack, you can animate directly, this works on browsers:

<!DOCTYPE html>
<style>div::before { content: "<::before>" }</style>
<div>text</div>
<script>
new Animation(new KeyframeEffect(
  document.querySelector("div"),
  [{ color: "blue" }, { color: "red" }],
  {
    duration: 2000,
    direction: "alternate",
    iterations: "Infinity",
    pseudoElement: "::before",
  },
), document.timeline).play();
</script>

Theoretically using Element.getAnimations() but that is not specced yet

It's specced (but seems unimplemented): https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions

element.getAnimations({pseudoElement: "::before"})

@bramus
Copy link
Contributor Author

bramus commented Jan 23, 2025

I don't think they can completely become first-class citizens in the DOM. Because in events, the target isn't a pseudo-element, and allowing it to be a CSSPseudoElement seems most likely not web compatible.

But [CSSPseudoElement] is specced to extend EventTarget, which would make it work?

Get info about their position+size

In some cases you can use getComputedStyle to get the height and width.

The getComputedStyle is not enough to, for example, get a ::view-transition-group()’s position mid-flight.

I don't think getBoundingClientRect() makes much sense for non-tree-abiding pseudo-elements like ::selection.

Good call. My assumption is that it would return null or throw when called on a non-tree-abiding pseudo-element.

Animate them: Only if they already have animation attached to them that you can hijack

You don't need to hijack, you can animate directly, this works on browsers:

🤦‍♂️ Duh, of course … and to say I even created a demo that uses it the very same day I posted this. Sorry, my bad!

Theoretically using Element.getAnimations() but that is not specced yet

It's specced (but seems unimplemented): https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions

element.getAnimations({pseudoElement: "::before"})

My proposal is to allow getAnimations to work directly on CSSPseudoElement instead needing to call getAnimations on its ultimate originating element.

@Loirooriol
Copy link
Contributor

But [CSSPseudoElement] is specced to extend EventTarget, which would make it work?

This would add support for addEventListener. But consider

document.addEventListener("mousemove", e => console.log(e.target))

When you move the mouse over #foo::before, you get an event where the target is the #foo element, not the pseudo-element.

Turning CSSPseudoElement into a EventTarget doesn't automatically change that, and changing that doesn't seem web compatible.

@bramus
Copy link
Contributor Author

bramus commented Jan 26, 2025

But consider

document.addEventListener("mousemove", e => console.log(e.target))

IUC the e.target is the ultimate originating element in this case, which could be left that way. A new property that distinguishes between the ultimate originating element and the pseudo (or some other way to detect this) could solve this.

@bramus
Copy link
Contributor Author

bramus commented May 12, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-pseudo-4 Current Work
Projects
None yet
Development

No branches or pull requests

2 participants