Re: [csswg-drafts] How to handle addEventListener on `CSSPseudoElement`? (#12163)

My previous rough draft was based on adding pseudo-elements to the event path and having them participate in bubbling.

After talking with the WHATNOT WG and hearing all the feedback, especially around the huge web-compat risks with `mouseover`/`mouseout` events (e.g. hovering mouse over pseudo-elements and real element would fire too many events), it's clear that my first approach was too complex and risky.

So, I've rethought it and would like to propose a much simpler and safer alternative.

### New Proposal: A "Contained Event" Model

The idea is simple: when an event happens on a pseudo-element, it fires a **self-contained event** that only its own listeners can hear.

* The event **does not bubble** up to the originating element. It lives and dies with the pseudo-element.
* To give developers context, we'd add a new `event.pseudoTarget` property that points to the pseudo-element handle.

This means the main event dispatch for the real element is **completely untouched**, which guarantees web compatibility. It neatly solves the `mouseover` problem because the pseudo's `mouseover` is a separate event that old code will never see.

**Here’s how it would work for the `::scroll-marker` use case:**

```javascript
const list = document.querySelector('#firstItem');
const scrollMarker = list.pseudo('::scroll-marker');

// The API is simple and does what you'd expect:
scrollMarker.addEventListener('click', (event) => {
  console.log('Scroll marker was clicked!');
  // event.target would still be the #firstItem if it bubbled,
  // but this event doesn't bubble at all.
  console.log(event.pseudoTarget); // The CSSPseudoElement for ::scroll-marker
});
```

***

### The Important Limitations: Where This Model Breaks Down

Now, this model is safer, but it isn't solving everything. Here are some examples of unsolved problems.

#### 1. Default event handling

Because the pseudo-element's event is completely separate, it can't affect the behavior of its real parent element.

* **Preventing Navigation:** If you have a `::before` on an `<a>` tag and call `event.preventDefault()` in the pseudo's `click` listener, the link **will still navigate**. The `preventDefault()` call only affects the contained event, not the separate, standard event that fires on the `<a>` tag.
* **Form Submission:** Likewise, if you style a pseudo-element to look like a submit button inside a `<form>`, clicking it **will not submit the form**. It's not a real form-associated element, and the click event doesn't trigger the form's submit action.

#### 2. Accessibility Issues

This is probably the most critical issue. The model encourages building components that are inaccessible to many users.

* **Invisible to Screen Readers:** Interactive logic attached to a pseudo-element is a black box for assistive technologies. A screen reader builds its understanding from the DOM via the **accessibility tree**, and pseudo-elements aren't really there now. Though, probably, it can be fixed with spec changes?
* **Keyboard Unreachability:** Since generally a pseudo-element can't get focus, it's unreachable via keyboard navigation. Any `click` listener on a pseudo is therefore **mouse-only logic**. This creates a significant barrier for users who rely on keyboards.

#### 3. Stateful UI

The model works for simple, one-off clicks but breaks down for any stateful UI interaction.

* **Drag and Drop:** Trying to use a `::before` as a "drag handle" is problematic. The `dragstart` event on the pseudo is disconnected from the element you actually want to drag.

#### 4. Ambiguous in Complex Layouts and Debugging

The model's simplicity creates ambiguity when interacting with other core browser systems.

* **Overlapping:** If you have multiple overlapping pseudo-elements with listeners, it's not clearly defined which one should receive the event, or if the parent should also get an event. Hit-testing rules become complex and unpredictable for developers.
* **Debugging:** Calling `event.composedPath()` on an event from a pseudo-element would be useless for debugging. It would show a very short path (maybe just the pseudo itself?), hiding its actual context within the larger DOM tree.

***

### New Recommendation: Start Small and Safe

Given these significant limitations, I don't think we should try to make all events work on all pseudo-elements.

Instead, I propose we take a cautious **"allow-list" approach**. We should start by enabling only a few simple, stateless events like `click` on specific pseudo-elements where we know it's safe and solves a real problem (like `::scroll-marker`).


-- 
GitHub Notification of comment by danielsakhapov
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/12163#issuecomment-3108997256 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Wednesday, 23 July 2025 14:54:32 UTC