Pseudo-elements in JavaScript

::before and ::after are not part of the DOM, so browsers give JavaScript almost no access to them. A click on an ::after icon is reported as a click on the element; you can't style one element's ::after from a script, measure it, or use :hover on it in CSS. css-pp makes them first-class, in every browser.

You want to Native browsers css-pp
Know a click hit the ::after, not the element No css.on('click', '.chip::after', fn)
Style a pseudo-element on hover or press No: ::after:hover is invalid CSS .chip::after:hover { ... }
Change one element's ::after from JavaScript No css.pseudo(el, '::after').set('color', 'red')
Read or change its text Read-only, via getComputedStyle() css.pseudo(el, '::after').text = '3 new'
Get its size and position No css.pseudo(el, '::after').rect()
Find all rendered ::afters No: querySelectorAll() can't select them css.pseudos('.card::after')
Close a modal when its backdrop is clicked Compare coordinates yourself css.on('click', 'dialog::backdrop', fn)

Chrome 149 added a native element.pseudo('::after') that identifies a pseudo-element, but Firefox and Safari don't support it, and in Chrome 156 you still can't attach a listener to it, measure it or restyle it.

Hover and press in CSS#

CSS
.chip::after { content: "×"; transition: background-color .15s; }
.chip::after:hover  { background: #dc2626; color: #fff; }   /* only while the pointer is on the × */
.chip::after:active { scale: .9; }                           /* only while it is pressed */

::before:hover, ::after:hover, ::before:active and ::after:active are supported. css-pp compiles them to .chip[data-css-after-hover]::after, so the element itself carries the state too. Style the element or its container from it: .card:has(.chip[data-css-after-hover]) { ... }.

Click and pointer events#

JavaScript
// close a chip when its "×" (drawn with ::after) is clicked
css.on('click', '.chip::after', function (e) {
  this.remove();          // this = the element that owns the ::after; e.pseudo = '::after'
});

// close a modal dialog when the user clicks outside it
css.on('click', 'dialog::backdrop', function () { this.close(); });

css.on(type, selector, handler) is a delegated listener on the document, so it also works for elements added later. The selector can be a list, and parts without a pseudo-element act as ordinary event delegation. It works with any event that has pointer coordinates: click, dblclick, contextmenu, pointerdown, pointerup, mousedown, mouseup. It returns a function that removes the listener:

JavaScript
var stop = css.on('click', '.chip::after', handler);
stop();

Accessibility. Pseudo-elements can't receive keyboard focus. Use pseudo-element clicks as a convenience, and keep a real <button> or a keyboard shortcut for the same action.

css.pseudo(element, type)#

Returns an object for one element's pseudo-element. The same object comes back on every call.

JavaScript
var badge = css.pseudo(document.querySelector('.inbox'), '::after');

badge.style('background-color');   // computed value, like getComputedStyle()
badge.set('background', '#16a34a'); // this element's ::after only, like an inline style
badge.set({ color: '#fff', 'font-weight': 700 });
badge.set('background', null);      // remove what you set
badge.text = '3 new';               // change its content text
badge.text;                         // "3 new"
badge.exists;                       // false if it isn't rendered (content: none)
badge.contains(x, y);               // is this viewport point on it?
badge.rect();                       // DOMRect: left, top, width, height…
badge.animate([{ scale: 1.4 }, { scale: 1 }], 300);
Pseudo-element style(), set(), text contains(), rect() css.on() events :hover, :active in CSS
::before, ::after Yes Yes Yes Yes
::backdrop Yes No Clicks No
::marker, ::placeholder, ::first-letter, ::first-line, ::selection Yes No No No

set() adds a rule for this one element with !important, so it wins over your stylesheet the way an inline style would.

css.pseudos(selector)#

Like querySelectorAll() for pseudo-elements: returns the css.pseudo() object of every matching element whose pseudo-element is actually rendered.

JavaScript
css.pseudos('.tag::after').forEach(function (p) { p.text = p.text.toUpperCase(); });

How it works#

  • Hit testing uses the browser's own engine. For an instant, css-pp makes the element and everything inside it ignore the pointer except the one pseudo-element, and asks document.elementFromPoint() what is there. Getting the element back means the point is on its pseudo-element. That is exact for absolutely positioned, inline, transformed and rotated pseudo-elements, and costs about 0.01 ms.
  • Hover runs that test at most once per animation frame while the pointer moves, only for elements your stylesheet uses with ::after:hover or ::after:active.
  • rect() measures a temporary stand-in element with exactly the same computed styles, in the same place, then removes it. It is accurate for positioned and inline pseudo-elements.
  • set() writes a rule that targets this element's stable data-css-i attribute.

If something else covers the pseudo-element, the topmost item wins, exactly as it does for clicks.