# Pseudo-elements in JavaScript: click, hover, style and measure ::before and ::after

> Detect clicks on ::before and ::after, style ::after:hover in CSS, change one element's pseudo-element from JavaScript, and read its size and position. Every browser.

Source: https://csspp.dev/docs/pseudo-elements/


`::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 `::after`s | 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 {#hover}

```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 {#events}

```js
// 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:

```js
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) {#pseudo}

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

```js
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) {#pseudos}

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

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

## How it works {#how}

- **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.
