DEV Community

Cover image for How to Detect Clicks on CSS Pseudo-Elements
theRealProHacker
theRealProHacker Subscriber

Posted on • Originally published at Medium

How to Detect Clicks on CSS Pseudo-Elements

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

StackOverflow screenshot of the question at hand

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.

  1. Get the attributes of the pseudo-element with getComputedStyle()
  2. 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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Try this out in the CodePen and have fun:

Edge cases

Briefly summarized:

  • Positioning: Assumes position: relative on the parent and absolute on 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)