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. CSSPseudoElement Interface and Use Cases.
- 3. Future Extensions.
- 4. Object Lifetime and Nature: Proxy vs. Direct Representation.
- 5. Handling pseudo element variations.
- 6. Event handling.
The CSSPseudoElement IDL interface aims to provide web developers with a mechanism to interact with pseudo elements (e.g., ::before, ::scroll-marker) 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).
CSSPseudoElement is the JavaScript interface for representing a pseudo-element. It is returned from Element.pseudo(type), where type is currently one of: ::after, ::before, ::marker. It acts as a proxy object — unlike a CSS pseudo-element, a CSSPseudoElement handle always exists once obtained, regardless of whether the pseudo-element is currently rendered.
CSSPseudoElement exposes the following attributes and methods:
type— a string representing the type of the pseudo-element (e.g."::before").element— the ultimate originating element of the pseudo-element.parent— the immediate originating element: either anElementor aCSSPseudoElementfor nested pseudo-elements.pseudo(type)— a method to retrieve nested pseudo-elements.selectorText— (resolved in #12161) the full, normalized selector string identifying this pseudo-element (e.g."::scroll-button(left)").
Some scenarios where CSSPseudoElement (together with event.pseudoTarget and other future extensions) unlocks new capabilities:
Event-driven interactions:
::scroll-markeranalytics — track which carousel pages are most visited by listening for clicks on each scroll marker.
document.querySelectorAll('.carousel-item').forEach(item => {
item.addEventListener('click', (event) => {
if (event.pseudoTarget?.type === '::scroll-marker') {
analytics.track('scroll-marker-click', { item: item.id });
}
});
});::scroll-buttoncustom behaviour — detect when a scroll button is clicked to implement infinite-loop detection or custom scroll-speed logic.
carousel.addEventListener('click', (event) => {
if (event.pseudoTarget?.selectorText === '::scroll-button(inline-end)') {
if (isAtEnd(carousel)) startLoop(carousel);
}
});::view-transition-*interaction — cancel an in-progress view transition when its outgoing snapshot is clicked.
document.addEventListener('click', (event) => {
if (event.pseudoTarget?.type === '::view-transition-old') {
document.startViewTransition(() => {}).skipTransition();
}
});::markerfold/unfold — collapse a list item when its marker bullet is clicked.
listItem.addEventListener('click', (event) => {
if (event.pseudoTarget?.type === '::marker') {
listItem.classList.toggle('collapsed');
}
});::backdropdismiss — close a dialog when its backdrop is clicked, without interfering with clicks inside the dialog content.
dialog.addEventListener('click', (event) => {
if (event.pseudoTarget?.type === '::backdrop') {
dialog.close();
}
});Animation and geometry:
- Geometry-aware view transition — once geometry methods land (see §3.2), read the bounding rect of
::view-transition-oldto set a custom animation origin.
document.addEventListener('transitionstart', (event) => {
if (event.pseudoTarget?.type === '::view-transition-old') {
const rect = event.pseudoTarget.getBoundingClientRect();
setTransformOrigin(rect);
}
});- Web Animations API on a pseudo-element — animate a
::beforedecoration directly (see §3.4 for the proposed first-class handle support).
// Today, string-based:
el.animate([{ opacity: 0 }, { opacity: 1 }], {
pseudoElement: '::before',
duration: 300,
});Testing and tooling:
- Assert pseudo-element is rendered — verify that a
::scroll-markeris generated for a list item.
const marker = listItem.pseudo('::scroll-marker');
// If .exists lands (see §4.1):
assert_true(marker.exists, '::scroll-marker should be rendered');- Polyfill guard — check whether a pseudo-element is natively rendered before activating a JS polyfill.
To support these cases, selected event types are extended with a pseudoTarget property, which is either a CSSPseudoElement (if the interaction occurred on a pseudo-element) or null.
This enables precise information about the event origin — not just that event.target (the ultimate originating element) was interacted with, but that specifically e.g. its ::after was the hit target. Importantly, event.target itself is unchanged; the event simply carries additional pseudo-element context.
The following event types expose pseudoTarget:
UIEvent(e.g.click,keydown,focus)AnimationEventTransitionEvent
Note:
mouseover,mouseout,mouseenter,mouseleave, and theirpointer*counterparts are not yet supported due to web compatibility concerns.
This section tracks capabilities that are planned or under active discussion for CSSPseudoElement but not yet specified or implemented.
Currently Element.pseudo(type) is only specified for ::after, ::before, and ::marker. Expanding the allow-list to other tree-abiding pseudo-elements is the most direct path to more use cases.
Candidates under discussion:
| Pseudo-element | Notes |
|---|---|
::scroll-marker |
Primary motivating use case for event.pseudoTarget. Pending explicit addition (#13346). |
::scroll-button(*) |
Same rationale as ::scroll-marker; parameterised, so requires selectorText (already resolved, but needs more thought). |
::scroll-marker-group |
Tree-abiding container; useful to check existence and get geometry. |
::column |
Plural pseudo-element — requires pseudoAll() or PseudoElementObserver to be useful. |
::view-transition-* |
High-value target for geometry access; plural and parameterised. |
::backdrop |
Useful for click-to-dismiss patterns on dialogs and popovers. |
::interest-hint |
Useful for some additional logic on interaction. |
The CSSOM View spec already includes CSSPseudoElement in the GeometryUtils mixin, but getBoundingClientRect() and getClientRects() are not yet surfaced on the interface in practice. Also, the GeometryUtils are not quite
finalized yet.
Adding these would enable:
- Positioning custom tooltips or overlays relative to a pseudo-element.
- Computing keyframe values for coordinated
::view-transition-*animations. - Measuring
::scroll-markerpositions for custom scroll-progress indicators. - Detecting whether a
::beforeused as a decorative element is within the viewport.
Proposed addition to CSSPseudoElement:
partial interface CSSPseudoElement {
DOMRectList getClientRects();
[NewObject] DOMRect getBoundingClientRect();
};Computed style for pseudo-elements is only accessible today via the string-based second parameter of getComputedStyle:
// This is the only specified way today:
getComputedStyle(el, '::before').getPropertyValue('content');There is no specified way to pass a CSSPseudoElement object as the first argument to getComputedStyle. The CSSOM spec defines getComputedStyle(Element elt, optional CSSOMString? pseudoElt) — the first parameter must be an Element. Future work is about making computed style access ergonomic through the CSSPseudoElement handle itself:
- Object-based access —
getComputedStyle(el.pseudo('::before'))passing the handle as the first argument. This is not yet specified. .exists/PseudoElementObserver— see §4.1 for the open question of how to expose render state.- Typed OM —
el.pseudo('::before').computedStyleMap()would give access to the CSS Typed OM for pseudo-elements, enabling structured reads and writes without string parsing.
The Web Animations API already accepts a pseudo-element target via KeyframeEffect's pseudoElement option (as a string). CSSPseudoElement should become a first-class target:
// Today (string-based):
new KeyframeEffect(el, keyframes, { pseudoElement: '::before' });
// Future (CSSPseudoElement handle):
const before = el.pseudo('::before');
before.animate(keyframes, options);
// or:
new KeyframeEffect(before, keyframes, options);This would make the handle consistent with how it is used in the events API (event.pseudoTarget) and geometry API, reducing the need for two parallel pseudo-addressing mechanisms.
Element.pseudo(type) returns a cached, persistent CSSPseudoElement handle for any valid type string — regardless of whether the pseudo element is currently rendered. This stable-identity model provides === equality across calls, consistent getComputedStyle() behaviour, and a stable target for event handling. The open questions below arise from this design.
Since CSSPseudoElement always exists as an object, developers need a way to determine whether the underlying pseudo element is actually rendered.
Options:
getComputedStyle()inspection — checkdisplay !== 'none'/content !== 'none'. Indirect, and forces a style computation.pseudoElement.exists(nullable boolean) — returnstrue/false/nullon a per-pseudo basis.nullfor pseudo elements where the notion of "existence" is privacy-sensitive (e.g.::spelling-error,::grammar-error) or indeterminate.- Async
PseudoElementObserver— proposed by @noamr as an alternative that avoids forcing synchronous style/layout, similar toResizeObserver, triggering during the style/layout loop.
Discussion: There are concerns that a synchronous .exists would force style/layout flushes and introduce subtle performance pitfalls. An async observer is preferred for tracking creation/removal of pseudo elements. However, a sync check may still be useful for specific point-in-time queries.
Recommendation:
Consider both: an async PseudoElementObserver for lifecycle tracking, and a carefully scoped sync .exists for point-in-time use cases. Privacy-sensitive pseudo elements should return null.
CSSWG issue — Resolved
RESOLVED: For a valid selector string, Element.pseudo() always returns a CSSPseudoElement object — even when called on an element that cannot generate that pseudo (e.g. input.pseudo('::before')). null is reserved exclusively for invalid/unrecognised type strings. Spec PR.
Should non-tree-abiding pseudo elements (e.g. ::selection) be supported by CSSPseudoElement, or should they have a separate interface? This question was reopened after §6.1 resolved that CSSPseudoElement no longer inherits from EventTarget.
Discussion: A unified interface is prefered for author ergonomics. Mixin-based inheritance tree has been proposed — a CSSPseudoElement base class with opt-in mixins such as CSSTreeAbidingProperties (.parent, .children) and CSSRangeBasedProperties (for ::selection), giving precise control over what each pseudo element type can and cannot do.
Recommendation:
Pursue a mixin-based approach. Leave CSSPseudoElement as a general base and apply mixins per pseudo element type to avoid over-promising capabilities.
CSSWG issue — Resolved
How should the argument of a parameterized pseudo element (e.g. ::scroll-button(left)) be exposed — embedded in type, or via a separate attribute?
RESOLVED: Add a selectorText property returning the full, normalized selector string (e.g. "::scroll-button(left)"). This gives developers clean, structured access without manual string parsing and round-trips correctly. The type attribute continues to return only the base pseudo element name. A spec PR is open.
Element.pseudo(type) assumes a single object per type, but some pseudo elements are inherently plural (e.g. ::column, ::view-transition-group(*)).
Discussion: The CSSWG initially resolved to add element.pseudoAll() returning a list of CSSPseudoElement. This was subsequently reverted pending more compelling use cases, due to unresolved questions around identity stability when the list grows/shrinks across layout changes, and the interaction with addEventListener.
Recommendation:
No pseudoAll() for now. Revisit once use cases and the identity model for plural pseudo elements are better understood. A PseudoElementObserver (see §4.1) may address some of the motivating scenarios.
Standard user interaction events fired over a pseudo element's area are retargeted to the originating Element before dispatch — event.target is always the element, never the pseudo element. Existing web content relies on this for event delegation.
CSSWG issue — Resolved
The question was how to let developers react to interactions specifically on pseudo elements without breaking event.target web compatibility.
Options considered:
- Status quo — no direct listening; developers infer from hit coordinates. Poor ergonomics.
- Direct
addEventListeneronCSSPseudoElementwith a special dispatch phase. Concerns raised about layering (annevk) and the complexity ofmouseover/mouseoutboundary events. - New event types (
pseudoElementClicketc.) — dismissed as leading to event proliferation. event.pseudoTarget— a new property on selected event types, set to theCSSPseudoElementif the interaction originated on a pseudo element, otherwisenull. Modelled onKeyframeEffect.pseudoElementin Web Animations.
RESOLVED: CSSPseudoElement does not inherit from EventTarget. Instead, event.pseudoTarget is added to a selected, allow-listed set of event types. event.target is unchanged — it remains the originating element — so all existing code continues to work unmodified.
Supported event types:
UIEvent(e.g.click,keydown,focus)AnimationEventTransitionEvent
mouseover, mouseout, mouseenter, mouseleave, and pointer* boundary counterparts are not supported due to web compatibility risk (they would fire excessively as the pointer moves between the pseudo element and the originating element).