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#
.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#
// 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:
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.
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.
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:hoveror::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 stabledata-css-iattribute.
If something else covers the pseudo-element, the topmost item wins, exactly as it does for clicks.