Abstract: This document provides a number of questions for discussion on the design considerations for the CSSPseudoElement IDL interface, as specified in the CSS Pseudo Elements Module Level 4. It addresses concerns regarding object lifetime, support for various pseudo element types (tree-abiding, non-tree-abiding, parameterized, plural), existence checks, event handling, web compatibility.
- 1. Introduction.
- 2. Object Lifetime and Nature: Proxy vs. Direct Representation.
- 3. Handling pseudo element variations.
- 4. Event handling.
The CSSPseudoElement IDL interface aims to provide web developers with a mechanism to interact with pseudo elements (e.g., ::before
, ::scroll-marker
, ::selection
) via JavaScript. This can be used to e.g. allow for querying their computed styles, using them in animations, event handling. The primary entry point is Element.pseudo(type)
.
A fundamental question is the nature of the CSSPseudoElement
object returned by Element.pseudo(type)
.
Current Spec Guidance says:
"Parse the type
argument as a <pseudo-element-selector>
, and let type
be the result. If type
is failure, return null
. Otherwise, return the CSSPseudoElement
object representing the pseudo element that would match the selector type
with this as its originating element."
This implies that if the selector syntax for the pseudo element type is valid, an object is returned. It doesn't explicitly state that the pseudo element must be currently generated or visible. The requirement to support getComputedStyle()
for a highlight pseudo (which might not always be "active," e.g., ::selection
when nothing is selected) further suggests the object can exist independently of the pseudo element's current rendering state.
The following table summarizes possible solutions to the problem to show why the currently specified approach is the best.
Model | Pros | Cons | Implementation Notes | Alignment with Spec/Resolutions |
---|---|---|---|---|
A: Cached, Persistent Handle (Current) | Stable identity (=== works). Simpler GC via weak refs. |
Indirect existence check needed (e.g., getComputedStyle ). Potential dev confusion. |
Requires caching mechanism (element-associated, weak refs). Object state is mostly static. | Aligns best with "same object" rule and #3607 resolution. |
B: Live Identity (Object if Rendered) | Direct existence check (!== null ). More intuitive "live" state? |
Unstable identity if pseudo appears/disappears. Potential object churn. | Return value depends on current style/layout. Requires checks on pseudo generation. | Conflicts with the "same object" rule. Considered in #3607. |
C: New Object if Rendered | Direct existence check (!== null ). No caching needed. |
No stable identity (=== fails). High object churn. Poor developer ergonomics. |
Return value depends on style/layout. Creates a new object each time if rendered. | Conflicts strongly with the "same object" rule. Least favored. |
Model A is the currently specified one, and it likely won’t be changed, as it has a number of useful pros:
- Stable Identity: Developers can get and hold onto a reference. This avoids scenarios where
Element.pseudo(type) === Element.pseudo(type)
might befalse
if the object was created on demand based on rendering. - Consistent API: Simplifies developer interaction as they don't need to constantly check for
null
just because a pseudo element might temporarily not be rendered (e.g., originating element isdisplay: none
, orcontent: none
for::before
). - Facilitates
getComputedStyle()
: Aligns with the ability to callgetComputedStyle()
even if the pseudo element isn't "visible" (e.g., for highlight pseudos, or to check what styles would apply). - EventTarget Potential: Allows
addEventListener
to be called on a stable object, even if the pseudo element isn't currently rendered (eventual event firing would depend on actual rendering).
But, nevertheless, it has a bunch of open questions, this explainer would like to address.
Developers might need a clear way to distinguish between having an object and the pseudo element actually being "live" or rendered?
Possible Solutions:
getComputedStyle()
inspection:- Check
getComputedStyle(pseudoElement).display !== 'none'
. For::before
/::after
, also checkgetComputedStyle(pseudoElement).content !== 'none'
. - Pros: Uses existing mechanisms.
- Cons: Indirect; might not cover all cases (e.g., a pseudo element that exists but has 0x0 dimensions and no visual content). For highlight pseudos, "display" might not be the relevant property. Sometimes depends on the originating element style.
- Check
- A dedicated
exists
property orisGenerated()
method:pseudoElement.exists
(nullable boolean, read-only)- Pros: Explicit and clear, allows to specify the return value on a per pseudo element basis.
- Cons: Adds new API surface. Defining "exists" precisely across all pseudo element types can be tricky (e.g., does
::selection
"exist" if nothing is selected but it could be styled? - potentially returnnull
here).
- Layout Information:
- Methods like a hypothetical
pseudoElement.getBoundingClientRects()
returning an empty list or specific values could indicate non-existence or non-rendering. - Pros: Provides practical information.
- Cons: Might be expensive to compute just for an existence check.
- Methods like a hypothetical
Recommendation:
Go with exists
, as it gives more control for each pseudo element while unifying an interface to get the needed information.
Otherwise: Relying on getComputedStyle()
initially seems pragmatic, given the limited list of pseudos supported currently. If specific use cases demonstrate its inadequacy, a dedicated property/method could be considered. For highlight pseudos, "existence" might mean "has associated ranges/segments."
What should elements that can’t have pseudos (e.g. <input>
) return from pseudo()
function? null
or CSSPseudoElement
object with parent
and element
as null
?
- Return
null
- Pros:
- Direct Feedback: Immediately signals to the developer that this specific combination of element and pseudo element type is not valid or won't render.
- Simpler "Existence" Check:
null
means it doesn't exist in this context; no further checks needed on a pseudo-object.
- Cons:
- Inconsistent API Signature:
Element.pseudo()
would returnnull
for two different reasons (invalid pseudo selector string or valid selector on an incompatible element), making error handling more complex. - Less Introspection: Prevents developers from getting a handle to inspect why it doesn't render (e.g., to check default computed styles for such a conceptual pseudo).
- Inconsistent API Signature:
- Pros:
- Return a
CSSPseudoElement
object (whose state/computed style then reflects non-generation)- Pros:
- API Consistency:
Element.pseudo(validPseudoTypeString)
always returns aCSSPseudoElement
object.null
is reserved only for invalid/unrecognized type strings. - Separation of Concerns:
Element.pseudo()
provides the handle;.exists
(potentially) on that handle reveals rendering state as per CSS rules. - Robust Introspection: Allows developers to get the object and check its computed style (e.g.,
display: "none"
,content: "none"
) to confirm and understand its non-rendered state. - Alignment with Highlight Pseudos: Consistent with
::selection
where an object can exist even if no selection is active. - Forward Compatibility: If CSS rules change, the API behavior for
Element.pseudo()
remains stable.
- API Consistency:
- Cons:
- Indirect "Existence" Check: Developers need to get the object and then inspect its computed styles(or potentially
.exists
, if it’ll be resolved) to confirm it's not actually rendered, rather than just checking fornull
. - Potentially "Useless" Objects: Creates an object that might not correspond to anything visible or affecting layout.
- Indirect "Existence" Check: Developers need to get the object and then inspect its computed styles(or potentially
- Pros:
Recommendation:
Return a CSSPseudoElement
Object. This approach is generally favored because it provides a more consistent and predictable API contract (null
only for invalid pseudo element type strings), and the author can use e.g. getComputedStyle
to determine whether the object exists. The discussion needs to be on if parent
and element
should be null
in that case.
Should we add support for non-tree-abiding pseudo elements? E.g. for a hypothetical getBoundingClientRects()
for ::selection
? Should CSSPseudoElement
be enriched with new methods or should new specific interfaces be added?
Recommendation:
Based on some discussions in web animations, non-tree-abiding pseudo elements should likely have a separate interface that is not inherited from EventTarget
, as it’s not possible sometimes to fit it into e.g. a bubbling model of events.
How to retrieve pseudo argument(s) for e.g. ::part(foo)
, ::view-transition-group(name)
, ::slotted(selector)
? Should it just be a part of type
value or a new attribute e.g. argument
should be added?
- Include parameter within the
type
attribute- Pros:
- Requires no changes to the existing interface structure (no new attributes).
- The complete identifier used to retrieve the pseudo element is available.
- Cons:
- Developers would need to manually parse the
type
string to separate the base pseudo element name (e.g.,::part
) from its argument (e.g.,foo
). - Less structured; mixes the type identifier with its arguments.
- Could become cumbersome if future pseudo elements allow more complex argument syntax (e.g., multiple arguments, different data types).
- Developers would need to manually parse the
- Pros:
- Add a new dedicated attribute (e.g.,
argument
orparameters
)- Description: Introduce a new nullable read-only attribute specifically for the parameter(s). This could be a single string (e.g.,
argument: CSSOMString?
) or potentially a sequence if multiple parameters were ever supported (e.g.,parameters: sequence<CSSOMString>?
). Forelement.pseudo('::part(foo)')
, this new attribute would return"foo"
, while thetype
attribute would likely return just"::part"
. - Pros:
- Provides clean, structured access to the parameter value(s).
- Separates the core type identifier (
type
) from its specific arguments. - More extensible and robust for handling potentially more complex parameterized pseudo elements in the future.
- Easier for developers to work with, avoiding manual string parsing.
- Cons:
- Requires adding a new attribute to the
CSSPseudoElement
interface, increasing its complexity slightly. - Requires defining the behavior for non-parameterized pseudos (e.g., should the attribute be
null
, an empty string, or an empty sequence?).
- Requires adding a new attribute to the
- Description: Introduce a new nullable read-only attribute specifically for the parameter(s). This could be a single string (e.g.,
Recommendation: Add a new dedicated attribute. Providing direct access to the parameter via a dedicated attribute improves developer ergonomics by avoiding manual string parsing and clearly separates the pseudo element's fundamental type from its specific instance arguments, also it is somewhat future-proof.
This is a significant challenge for the current Element.pseudo(type)
which implies a single object per type. Examples are ::view-transition-old(*)
/ ::view-transition-new(*)
(where *
could match multiple captured elements with the same tag name), ::column
pseudos and each can have distinct ::scroll-marker
inside.
Element.pseudo(type)
returns the first or a representative one.- Pros: Simple, fits the current IDL.
- Cons: Incomplete, doesn't allow access to other instances.
- Introduce
Element.pseudos(type)
returning aNodeList
-like collection (CSSPseudoElementList
?).- Pros: Explicitly handles plurality. Aligns with DOM patterns like
querySelectorAll
. - Cons: Adds new API. How would items in the list be uniquely identified or ordered if they are not tree-abiding?
- Pros: Explicitly handles plurality. Aligns with DOM patterns like
- Parameterize potentially ambiguous pseudos, and let the parameter distinguish them (e.g. make
::column(selector)
).- Pros: Uses an existing mechanism for some cases.
- Cons: Not a general solution.
Recommendation:
We likely need an Element.pseudos(type)
or a similar mechanism for pseudo elements that are inherently plural, as an author might want to add a click event listener to gather clicks statistics from every ::scroll-marker
nested into ::column
.
The Core Conflict: event.target
A fundamental aspect of current web platform behavior is that user interaction events (like click
, mouseover
, etc.) that physically occur over the area styled or occupied by a pseudo element are retargeted before dispatch. The event.target
property of the dispatched event is set to the originating Element
, not the pseudo element itself. Existing web content heavily relies on this behavior for event delegation and handling.
Given that the CSSPseudoElement
object acts as a persistent handle, attaching an event listener via addEventListener
raises further questions. What does it mean to listen for an event on a handle when the actual pseudo element might not be currently rendered (e.g., due to display: none
)?
Should the listener only become active when the pseudoelement is actually rendered?
How does this interact with the retargeting behavior? If event.target
remains the Element
, how does a listener on the CSSPseudoElement
get invoked? Does it require a special dispatch phase?
Proposed variants are included in the table:
Model | event.target Behavior (for existing events like click ) |
addEventListener on Pseudo? |
Pros | Cons | Feasibility Notes |
---|---|---|---|---|---|
A: Status Quo | Element (retargeted) |
No | Full web compatibility. Simple. | No direct script interaction with pseudo events. | Current state. |
B: Full EventTarget |
CSSPseudoElement |
Yes | Direct event handling on pseudo. Conceptually clean object model. | Major breaking change for web compatibility. Complex bubbling definition needed, esp. for non-tree pseudos. | Highly unlikely due to web compatibility constraints. |
C: Hybrid (Parent + Selector) | Element (retargeted) |
No (Interaction via parent) | Avoids web compat issues. Sidesteps pseudo lifetime/eventing complexity for the use case. | Less direct object model. Requires specific API pattern (like Web Animations pseudoElement ). |
Proven feasible for Web Animations L1. Potentially applicable to other interactions. |
D: Limited EventTarget |
Element (for existing events). CSSPseudoElement (for new specific events only). |
Yes (for specific events) | Maintains web compat for existing events. Allows direct handling for new use cases. | Complex dispatch logic. Bubbling complexity remains for events targeting pseudo. Potential developer confusion. | Requires careful definition of which events target what, and clear bubbling rules. Avoids the main breaking change of Model B. |
This would require some heavy discussions, as it’s already known that authors want to be able to count clicks on ::scroll-marker
pseudo elements for analytics purposes.
Potential solutions for ::scroll-marker
:
- No direct listening on pseudo element (Status Quo):
- Description: Do not allow
addEventListener('click',...)
directly on theCSSPseudoElement
for::scroll-marker
. Clicks physically hitting the marker would continue to dispatch withevent.target
set to the originating scrollableElement
. Developers would have to attach the listener to theElement
and use coordinate-based hit-testing within that listener to infer if the click landed on the marker area. - Pros:
- Requires no changes to the event dispatch model.
- Zero web compatibility risk regarding
event.target
.
- Cons:
- Very poor developer ergonomics for this specific use case. Calculating marker positions and sizes to perform manual hit-testing is complex, brittle (breaks with style changes), and potentially slow.
- Makes the
CSSPseudoElement
object less useful and doesn't leverage itsEventTarget
inheritance.
- Description: Do not allow
- Allow direct listening with special dispatch phase:
- Description: Allow developers to call
scrollMarkerPseudoElement.addEventListener('click', counterFunction)
. When a click physically occurs on the scroll marker:- Special Phase: The browser's event system identifies the hit target as the
::scroll-marker
and invokescounterFunction
(and any other listeners attached directly to this specificCSSPseudoElement
handle). - Standard Phase: The event dispatch continues as normal, but crucially,
event.target
is set to the originating scrollableElement
. The event then proceeds through the capture and bubble phases relative to theElement
, without re-invokingcounterFunction
during this standard flow.
- Special Phase: The browser's event system identifies the hit target as the
- Pros:
- Directly addresses the developer need: allows attaching listeners specifically to the pseudo-element of interest.
- Preserves web compatibility by ensuring
event.target
remains theElement
during the standard, observable event flow. - Provides a meaningful use for the
EventTarget
inheritance onCSSPseudoElement
. - Much better developer ergonomics than manual coordinate checking.
- Cons:
- Requires implementing the modified event dispatch logic (the "special phase").
- The event model becomes slightly more complex internally, though the developer-facing API (
addEventListener
on the pseudo) is intuitive.
- Description: Allow developers to call
- Introduce a new specific event type:
- Description: Instead of using the standard
click
event, define a new event type likescrollMarkerClick
that only fires on theCSSPseudoElement
handle for the::scroll-marker
. Standardclick
events would continue to target theElement
only. - Pros:
- Avoids any ambiguity or modification related to the standard
click
event dispatch. - Clearly separates the interaction.
- Avoids any ambiguity or modification related to the standard
- Cons:
- Introduces a new event type for a very specific interaction, potentially leading to event type proliferation.
- Developers might intuitively try to listen for
click
first and be confused why it doesn't work directly on the pseudo-element handle without the special dispatch phase. - Doesn't fully resolve the general question of how other standard events (like
mouseover
) should interact with pseudo-elements if direct listeners are desired.
- Description: Instead of using the standard
Recommendation:
Option 2: Allow Direct Listening with Special Dispatch Phase.
Justification: This option provides the best balance. It directly enables the desired developer use case (listening for clicks on the marker) with good ergonomics (addEventListener
on the pseudo-element object). Crucially, it achieves this while adhering to the non-negotiable web compatibility requirement that event.target
for standard events like click
must remain the originating Element
. While it requires implementing a modification to the event dispatch mechanism, this internal complexity seems justified to provide a usable and compatible solution. Option 1 is too cumbersome for developers, and Option 3 introduces a new event type which might be unnecessary if the dispatch modification in Option 2 can be generalized for other standard events targeting pseudo-elements.