Imagine you want to add a close button to a modal without adding anything to the DOM. So, you use a pseudo-element like this for example:
.dismissable:hover::after {
content: "x";
font-size: 3rem;
display: block;
position: absolute;
top: 10px;
left: 10px;
cursor: pointer;
}
Now, you want to actually make the close button call some JavaScript on click. However, this is much more difficult than you might initially anticipate. For example, you can't just do something like this:
let close_button = document.querySelector(".dismissable::after")
close_button.onclick = ()=>{
// close the modal
}
Naturally, I turned to the internet, and stumbled onto exactly the question I had: “jquery detect ::after css selector click event”. This question is a prime example of how bad StackOverflow actually is. The situation depresses me a lot, to the point that I don’t ask any questions anymore. The question was closed as a duplicate, even though the referenced question is totally different.
Neither the original question nor the referenced question had any answers that were able to solve my problem. I still gave both the original question and the single answer a like, as they could be valuable to others, and I wanted to show those people some love for their time and effort.
Detecting that the pseudo-element was clicked
So now let's get to the real stuff: I'll show you a possible solution with its edge cases. The essential idea is a mixture of two ideas.
- Get the attributes of the pseudo-element with
getComputedStyle() - Check the mouse position relative to the pseudo-element
We can get the pseudo-element's style with getComputedStyle(elem, "::after"). This is extremely useful in our situation.
With .getPropertyValue() we can then get the computed values of top, height, left and width, which allow us to determine the bounds of the pseudo-element. Importantly, the computed values are just strings that look like {num}px. To get just the number, we slice off the last two characters and then parse the strings as Numbers.
We get the mouse positions relative to the .dismissable by accessing the layerX and layerY attributes.
The final step is a simple bounds check. If it is successful, we can do whatever we desire. In this case, I made the .dismissable simply disappear by setting its display attribute to "none", but normally you might want to do different things.
const handler = (e)=>{
// First we get the pseudo-element's style
const target = e.currentTarget || e.target
const after = getComputedStyle(target, "::after")
if (after) {
// Then we parse out the dimensions
const atop = Number(after.getPropertyValue("top").slice(0, -2))
const aheight = Number(after.getPropertyValue("height").slice(0, -2))
const aleft = Number(after.getPropertyValue("left").slice(0, -2))
const awidth = Number(after.getPropertyValue("width").slice(0, -2))
// And get the mouse position
const ex = e.offsetX
const ey = e.offsetY
// Finally we do a bounds check (Is the mouse inside of the after element)
if (ex > aleft && ex < aleft+awidth && ey > atop && ey < atop+aheight) {
console.log("Button clicked")
target.style.display = "none"
}
}
}
Try this out in the CodePen and have fun:
Edge cases
Briefly summarized:
- Positioning: Assumes
position: relativeon the parent andabsoluteon the pseudo-element. - Shape: Assumes the pseudo-element is a rectangle.
- Transforms: Assumes no CSS transforms on the pseudo-element.
Have you found a cleaner way to handle this? Drop it in the comments!

Top comments (0)