285

Please see this fiddle: http://jsfiddle.net/ZWw3Z/5/

My code is:

p {
    position: relative;
    background-color: blue;
}

p:before {
    content: '';
    position: absolute;
    left:100%;
    width: 10px;
    height: 100%;
    background-color: red;
}
    <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate...</p>

I would like to trigger a click event only on the pseudo-element (the red bit). That is, I don't want the click event to be triggered on the blue bit.

Nat Riddle
  • 928
  • 1
  • 10
  • 24
Randomblue
  • 112,777
  • 145
  • 353
  • 547
  • 1
    How about checking clicked pos? http://stackoverflow.com/questions/3234977/using-jquery-how-to-get-click-coordinates-on-the-target-element –  Dec 08 '13 at 16:51
  • 1
    Please read my previously posted solution [here](http://stackoverflow.com/a/42953031/1125344). – Amr Mar 22 '17 at 13:48
  • Possibly a good workaround [HERE](https://stackoverflow.com/questions/12692762/use-javascript-to-click-on-a-pseudo-element/51176455#51176455). – Reza Mamun Jul 04 '18 at 15:05
  • 1
    try `$('p').before().click(function...)` – Mladen B. Aug 27 '20 at 05:13
  • @RezaMamun Can you check this and suggests to me how can I achieve it. https://stackoverflow.com/questions/66923436/on-hover-after-element-move-to-current-div – Husna Apr 03 '21 at 18:42

13 Answers13

262

This is not possible; pseudo-elements are not part of the DOM at all so you can't bind any events directly to them, you can only bind to their parent elements.

If you must have a click handler on the red region only, you have to make a child element, like a span, place it right after the opening <p> tag, apply styles to p span instead of p:before, and bind to it.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • 147
    It seems that in modern browsers this is kind of possible with different `pointer-events` values for the element itself and its pseudo element: http://jsfiddle.net/ZWw3Z/70/ – Ilya Streltsyn Aug 24 '14 at 13:16
  • 8
    @Ilya Streltsyn amazing - not the 'correct' answer (wouldn't work if you also needed to click the element) but works brilliantly for a 'close button' on divs – RozzA Jan 09 '15 at 04:28
  • `pointer-events` don't appear to do the trick for IE9, according to the jsfiddel that Ilya posted. – Matt Sgarlata Aug 05 '15 at 14:32
  • @user393274: IE9 doesn't support pointer-events outside of SVG. – BoltClock Aug 05 '15 at 16:22
  • There's a difference between "not possible" and "not supported". I think it would be more correct to say "not supported" in this case. – Trevor Jan 19 '17 at 19:08
  • @Trevor: It's fundamentally not possible to bind event handlers to pseudo-elements. pointer-events is a completely different story with caveats of its own. If pointer-events does the job for you, feel free to use it; I'm just answering the question like it is (and besides, pointer-events wasn't even all that well supported five years ago and therefore not a viable solution). – BoltClock Jan 20 '17 at 03:52
  • I wasn't referring to pointer-events, and I think you're answer is very helpful to the conversation. – Trevor Jan 20 '17 at 21:19
  • @IlyaStreltsyn Is this what is done here: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/concepts.html (on the "+" pseudo elements in the sidebar)? Because I don't see "pointer-events" in the css. – theyuv Jul 18 '17 at 07:31
  • 1
    @theyuv no, on that page the whole line is clickable, both on the left and on the right from the link. Only the link itself has a different click handler, so clicking it doesn't trigger folding/unfolding subtree. – Ilya Streltsyn Jul 18 '17 at 16:21
187

Actually, it is possible. You can check if the clicked position was outside of the element, since this will only happen if ::before or ::after was clicked.

This example only checks the element to the right but that should work in your case.

span = document.querySelector('span');

span.addEventListener('click', function (e) {
    if (e.offsetX > span.offsetWidth) {
        span.className = 'c2';
    } else {
        span.className = 'c1';
    }
});
div { margin: 20px; }
span:after { content: 'AFTER'; position: absolute; }

span.c1 { background: yellow; }
span.c2:after { background: yellow; }
<div><span>ELEMENT</span></div>

JSFiddle

brasofilo
  • 25,496
  • 15
  • 91
  • 179
Linus Unnebäck
  • 23,234
  • 15
  • 74
  • 89
  • 1
    It doesn't work for me, the text gets highlighted when I click on either. Using Firefox, if it matters (shouldn't). – Flater Apr 23 '14 at 12:59
  • 2
    for firefox: http://jsfiddle.net/wC2p7/16/ . If working with nested elements, you might need to calculate nested offsets as appropriate (eg. jQuery: $(e.target).offset().left ) – bebbi Jul 01 '14 at 15:38
  • 1
    Awesome man thanks, it works. But in Firefox and other standards compliant browsers, for testing if mouse over `:after` you have to check `if (e.clientX > span.offsetLeft + span.offsetHeight)` as these browsers don't have the `e.offsetX` property. Fiddle: http://jsfiddle.net/Noitidart/wC2p7/18/ – Noitidart Jul 16 '14 at 06:25
  • @Noitidart I think you ment `offsetLeft + offsetWidth` but good catch! Maybe it would be time to create a small js library that abstracts away the browser difference. – Linus Unnebäck Jul 21 '14 at 07:42
  • 5
    Even cooler would be if jQuery decided to support `$('a:after').on('click', fn)` but I don't really see that happening :p – Linus Unnebäck Jul 21 '14 at 07:43
  • Ah yes thanks for the catch on my catch haha! `span.offsetLeft + span.offsetWidth` is correct, just verifying so if any others come across this topic and things dont work on first run they don't waste time by really trying to add `offsetLeft` and `offsetHeight` haha. + rep to @bebbi i didnt notice he posted a firefox solution – Noitidart Jul 21 '14 at 08:03
  • 45
    This will actually not work if `::before` or `::after` is positioned inside the element. – laconbass Jan 08 '15 at 17:19
  • I upvoted this because its clever, but it doesnt work exactly as everyone would probably expect. It DOES NOT allow you to click "after" elements that are overlapping the parent element. This GREAT for targeting elements that arent absolute positioned unless theyre moved out of the parent's bounding area. –  Apr 13 '18 at 19:10
  • This works for me if you change `e.offsetX > span.offsetWidth` to `e.offsetX < 0` – Kohjah Breese Aug 15 '18 at 15:36
  • 1
    @laconbass That is true, but in my case, the ::before pseudo was positioned inside the element, but it has a fixed width of 20px. Therefore I can still target clicks on that pseudo simply by limiting the clicked X offset to 20: `e.offsetX < 20` – Kristián Filo Oct 26 '20 at 10:51
  • 1
    @Noitidart 2021 update: offsetX seems to be safe now :) https://caniuse.com/mdn-api_mouseevent_offsetx – LuH Jan 20 '21 at 12:39
  • smart solution. it worked for me – Mohamed Elleuch Jul 10 '22 at 18:03
97

On modern browsers you can try with the pointer-events css property (but it leads to the impossibility to detect mouse events on the parent node):

p {
    position: relative;
    background-color: blue;
    color:#ffffff;
    padding:0px 10px;
    pointer-events:none;
}
p::before {
    content: attr(data-before);
    margin-left:-10px;
    margin-right:10px;
    position: relative;
    background-color: red;
    padding:0px 10px;
    pointer-events:auto;
}

When the event target is your "p" element, you know it is your "p:before".

If you still need to detect mouse events on the main p, you may consider the possibility to modify your HTML structure. You can add a span tag and the following style:

p span {
    background:#393;
    padding:0px 10px;
    pointer-events:auto;
}

The event targets are now both the "span" and the "p:before" elements.

Example without jquery: http://jsfiddle.net/2nsptvcu/

Example with jquery: http://jsfiddle.net/0vygmnnb/

Here is the list of browsers supporting pointer-events: http://caniuse.com/#feat=pointer-events

Full
  • 455
  • 4
  • 13
Fasoeu
  • 1,222
  • 9
  • 8
  • I dont know why parent node is. Are you saying that if I the click event on the something like .box:before, the then .box can not detect any click? – most venerable sir Jul 10 '17 at 00:21
  • 1
    That's partially true: if your CSS contains something like .box{pointer-events:none;} .box:before{pointer-events:auto;} then the only element which still supports events is p:before. Remember anyway that js will give you back the main .box element since .box:before doesn't really live in The DOM. – Fasoeu Jul 11 '17 at 06:05
  • You can actually still detect mouse events on the original element. You don't need to always have this rule set, you can simply set it the time to check if the element is still hovered even when there is only its pseudo elements that do receive pointer-events: https://jsfiddle.net/0z8p5ret/ This will cause a big number of reflows though, so any other way would probably be better. – Kaiido Apr 05 '21 at 06:02
  • Unfortunately, `pointer-events:none` also disables `hover` events – Aurovrata Jul 25 '23 at 08:39
12

Short Answer:

I did it. I wrote a function for dynamic usage for all the little people out there...

Working example which displays on the page

Working example logging to the console

Long Answer:

...Still did it.

It took me awhile to do it, since a psuedo element is not really on the page. While some of the answers above work in SOME scenarios, they ALL fail to be both dynamic and work in a scenario in which an element is both unexpected in size and position(such as absolute positioned elements overlaying a portion of the parent element). Mine does not.

Usage:

//some element selector and a click event...plain js works here too
$("div").click(function() {
    //returns an object {before: true/false, after: true/false}
    psuedoClick(this);

    //returns true/false
    psuedoClick(this).before;

    //returns true/false
    psuedoClick(this).after;

});

How it works:

It grabs the height, width, top, and left positions(based on the position away from the edge of the window) of the parent element and grabs the height, width, top, and left positions(based on the edge of the parent container) and compares those values to determine where the psuedo element is on the screen.

It then compares where the mouse is. As long as the mouse is in the newly created variable range then it returns true.

Note:

It is wise to make the parent element RELATIVE positioned. If you have an absolute positioned psuedo element, this function will only work if it is positioned based on the parent's dimensions(so the parent has to be relative...maybe sticky or fixed would work too....I dont know).

Code:

function psuedoClick(parentElem) {

    var beforeClicked,
      afterClicked;

  var parentLeft = parseInt(parentElem.getBoundingClientRect().left, 10),
      parentTop = parseInt(parentElem.getBoundingClientRect().top, 10);

  var parentWidth = parseInt(window.getComputedStyle(parentElem).width, 10),
      parentHeight = parseInt(window.getComputedStyle(parentElem).height, 10);

  var before = window.getComputedStyle(parentElem, ':before');

  var beforeStart = parentLeft + (parseInt(before.getPropertyValue("left"), 10)),
      beforeEnd = beforeStart + parseInt(before.width, 10);

  var beforeYStart = parentTop + (parseInt(before.getPropertyValue("top"), 10)),
      beforeYEnd = beforeYStart + parseInt(before.height, 10);

  var after = window.getComputedStyle(parentElem, ':after');

  var afterStart = parentLeft + (parseInt(after.getPropertyValue("left"), 10)),
      afterEnd = afterStart + parseInt(after.width, 10);

  var afterYStart = parentTop + (parseInt(after.getPropertyValue("top"), 10)),
      afterYEnd = afterYStart + parseInt(after.height, 10);

  var mouseX = event.clientX,
      mouseY = event.clientY;

  beforeClicked = (mouseX >= beforeStart && mouseX <= beforeEnd && mouseY >= beforeYStart && mouseY <= beforeYEnd ? true : false);

  afterClicked = (mouseX >= afterStart && mouseX <= afterEnd && mouseY >= afterYStart && mouseY <= afterYEnd ? true : false);

  return {
    "before" : beforeClicked,
    "after"  : afterClicked

  };      

}

Support:

I dont know....it looks like ie is dumb and likes to return auto as a computed value sometimes. IT SEEMS TO WORK WELL IN ALL BROWSERS IF DIMENSIONS ARE SET IN CSS. So...set your height and width on your psuedo elements and only move them with top and left. I recommend using it on things that you are okay with it not working on. Like an animation or something. Chrome works...as usual.

  • `event` is passed through an event handler function everytime an event is fired off. Like clicking or typing. When we use the `.click()` function, we are waiting for the click event. We could specify and say `.click(function(e){...})` to make event be `e` but it is also acceptable to just use `event`. –  Nov 05 '18 at 22:28
  • 5
    psEUdo, not psUEdo! – biziclop Nov 08 '18 at 12:22
  • The code above doesn't work, because `event` is undefined. – Sebastian Zartner Jan 07 '19 at 18:22
  • @SebastianZartner - Event is a keyword. It is when the user does something. I literally explained this two comments above your comment. When the user interacts with the page, an event is fired off(in most cases). Event is a keyword coming through from the event listener ".click()". If a developer wants to use a different name for event, they can set it up in the event listener like ".click(e)" which is a more common way to see it. HOWEVER....even though I have a link working above, I will also include one that is more obvious and doesnt log to the console but instead to the page for the needy. –  Jan 21 '19 at 15:46
  • I should have mentioned that it doesn't work in Firefox, because there `event` is undefined. It works in Chrome and Edge, though. – Sebastian Zartner Jan 21 '19 at 19:52
  • the key element is `var before = window.getComputedStyle(parentElem, ':before');` awesome. Thank you. – Mohamed Allal Dec 01 '22 at 23:36
9

My answer will work for anyone wanting to click a definitive area of the page. This worked for me on my absolutely-positioned :after

Thanks to this article, I realized (with jQuery) I can use e.pageY and e.pageX instead of worrying about e.offsetY/X and e.clientY/X issue between browsers.

Through my trial and error, I started to use the clientX and clientY mouse coordinates in the jQuery event object. These coordinates gave me the X and Y offset of the mouse relative to the top-left corner of the browser's view port. As I was reading the jQuery 1.4 Reference Guide by Karl Swedberg and Jonathan Chaffer, however, I saw that they often referred to the pageX and pageY coordinates. After checking the updated jQuery documentation, I saw that these were the coordinates standardized by jQuery; and, I saw that they gave me the X and Y offset of the mouse relative to the entire document (not just the view port).

I liked this event.pageY idea because it would always be the same, as it was relative to the document. I can compare it to my :after's parent element using offset(), which returns its X and Y also relative to the document.

Therefore, I can come up with a range of "clickable" region on the entire page that never changes.


Here's my demo on codepen.


or if too lazy for codepen, here's the JS:

* I only cared about the Y values for my example.

var box = $('.box');
// clickable range - never changes
var max = box.offset().top + box.outerHeight();
var min = max - 30; // 30 is the height of the :after

var checkRange = function(y) {
  return (y >= min && y <= max);
}

box.click(function(e){
  if ( checkRange(e.pageY) ) {
    // do click action
    box.toggleClass('toggle');
  }
});
amurrell
  • 2,383
  • 22
  • 26
9

This works for me:

$('#element').click(function (e) {
        if (e.offsetX > e.target.offsetLeft) {
            // click on element
        }
         else{
           // click on ::before element
       }
});
Rick
  • 1,042
  • 2
  • 14
  • 34
  • this is very specific for your positioning case and does not work everywhere at all, but working with `e.offsetX/Y` and `e.target.offsetLeft`/`e.target.offsetWidth` is a good idea for start for each specific case – user151496 Jun 24 '22 at 08:23
6

This is edited answer by Fasoeu with latest CSS3 and JS ES6

Edited demo without using JQuery.

Shortest example of code:

<p><span>Some text</span></p>
p {
    position: relative;
    pointer-events: none;
}
p::before {
    content: "";
    position: absolute;
    pointer-events: auto;
}
p span {
    display: contents;
    pointer-events: auto;
}
const all_p = Array.from(document.querySelectorAll('p'));

for (let p of all_p) {
    p.addEventListener("click", listener, false);
};

Explanation:

pointer-events control detection of events, removing receiving events from target, but keep receiving from pseudo-elements make possible to click on ::before and ::after and you will always know what you are clicking on pseudo-element, however if you still need to click, you put all content in nested element (span in example), but because we don't want to apply any additional styles, display: contents; become very handy solution and it supported by most browsers. pointer-events: none; as already mentioned in original post also widely supported.

The JavaScript part also used widely supported Array.from and for...of, however they are not necessary to use in code.

XCanG
  • 429
  • 6
  • 16
3

I solved this case with add pointer-events: none; at :after css

  • Can you explain more ? Currently multiple other answer use the same solution. So what make your answer add something for another user ? Please edit your answer :) – Elikill58 Oct 01 '21 at 10:04
  • 1
    This solution is great as it relies on a standard CSS property for browsers that support `pointer-events`. For example, `p { pointer-events: none; } p::before { pointer-events: auto; }`. – morganney Nov 24 '21 at 21:29
  • it actually worked, flaweless – norbertas.gaulia Sep 15 '22 at 13:43
1

Add condition in Click event to restrict the clickable area .

    $('#thing').click(function(e) {
       if (e.clientX > $(this).offset().left + 90 &&
             e.clientY < $(this).offset().top + 10) {
                 // action when clicking on after-element
                 // your code here
       }
     });

DEMO

Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
-1

None of these answers are reliable, and mine wont be much more reliable.

Caveats aside, if you do get into the lucky scenario where the element you're trying to have clicked doesn't have padding (such that all of the "inner" space of the element is completely covered by sub-elements), then you can check the target of the click event against the container itself. If it matches, that means you've clicked a :before or :after element.

Obviously this would not be feasible with both types (before and after) however I have implemented it as a hack/speed fix and it is working very well, without a bunch of position checking, which may be inaccurate depending on about a million different factors.

dudewad
  • 13,215
  • 6
  • 37
  • 46
-1

I'll post the solution with:

  1. offset() method to get the top-left corner info of the root element.
  2. window.getComputedStyle() method to get the styles of the pseudo-element.

Please make sure the position property of the root element is specified as relative, and absolute for the pseudo-element.

The Fiddle demo is here.
Hope this will help.

Additional comment:
This question was posted 11 years ago, and already has an accepted answer.
The reason why I posted this solution despite the above fact is as follows.

  • I had struggled with the same desire to detect click event on pseudo-element.
  • The accepted answer ("adding a child element") does not satisfy my needs.
  • I've got the idea of the solution thanks to some posts on this thread.
  • I would be happy if someone with a similar problem could solve it with this post.

/**
 * Click event listener.
 */
$(element).click((e) => {
    // mouse cursor position
    const mousePoint = { x: e.pageX, y: e.pageY };

    if (isMouseOnPseudoElement(mousePoint, e.target, 'before')) {
        console.log('[:before] pseudo-element was clicked');
    } else if (isMouseOnPseudoElement(mousePoint, e.target, 'after')) {
        console.log('[:after] pseudo-element was clicked');
    }
});

/**
 * Returns the info of whether the mouse cursor is over the pseudo-element.
 *
 * @param {JSON} point: coordinate of mouse cursor
 * @param {DOMElement} element: root element
 * @param {String} pseudoType: "before" or "after"
 * @returns {JSON}: info of whether the mouse cursor is over the pseudo-element
 */
function isMouseOnPseudoElement(point, element, pseudoType = 'before') {
    const pseudoRect = getPseudoElementRect(element, pseudoType);

    return point.y >= pseudoRect.top
        && point.y <= pseudoRect.bottom
        && point.x >= pseudoRect.left
        && point.x <= pseudoRect.right;
}

/**
 * Gets the rectangle info of the pseudo-element.
 *
 * @param {DOMElement} element: root element
 * @param {String} pseudoType: "before" or "after"
 * @returns {JSON}: rectangle info of the pseudo-element
 */
function getPseudoElementRect(element, pseudoType = 'before') {
    // top-left info of the root element
    const rootOffset = $(element).offset();

    // style of the pseudo-element
    const pseudoStyle = window.getComputedStyle(element, ':' + pseudoType);

    const top = rootOffset.top + parseFloat(pseudoStyle.top);
    const left = rootOffset.left + parseFloat(pseudoStyle.left);

    const height = parseFloat(pseudoStyle.height);
    const width = parseFloat(pseudoStyle.width);
    const borderTop = parseFloat(pseudoStyle['border-top-width']);
    const borderBottom = parseFloat(pseudoStyle['border-bottom-width']);
    const borderLeft = parseFloat(pseudoStyle['border-left-width']);
    const borderRight = parseFloat(pseudoStyle['border-right-width']);

    // rounds the decimal numbers detected when zooming
    return {
        top: Math.round(top),
        left: Math.round(left),
        bottom: Math.round(top + height + borderTop + borderBottom),
        right: Math.round(left + width + borderLeft + borderRight)
    };
}
Shin
  • 11
  • 1
  • 1
  • 6
  • This question is asked more than 11 years ago and it has an accepted answer. Please add some details about the reason you are adding a new answer – MD Zand Nov 29 '22 at 20:16
  • @MDZand, thank you very much for pointing this out. I added the reason to my post. – Shin Nov 30 '22 at 01:01
-1

Without JQuery i used this for sidebar menu click detectionon pseudo plus icons:

HTML:

<ul>
    <li>MENU ELEMENT</li>
    <li>MENU ELEMENT</li>
    <li>MENU ELEMENT</li>
</ul>

CSS:

ul { margin: 30px; }
li { display: flex; width: 300px; justify-content: space-between;}
li:after { content: ' +'}

li.c1 { background: red; }
li.c2:after { background: yellow; }

JS:

document.querySelectorAll("li").forEach(function (e) {
        e.addEventListener('click', function(u) {
            let linkWidth = this.offsetWidth;           
            let pseudoWidth = parseFloat(window.getComputedStyle(this, ':after').width);
            const mouseX = u.offsetX;
            if (mouseX > (linkWidth - pseudoWidth)) {
                console.log ("click pseudo");
                this.className = 'c2';
            } else {
                console.log ("click element");
                this.className = 'c1';
            }
        })
});
-4

No,but you can do like this

In html file add this section

<div class="arrow">
</div>

In css you can do like this

p div.arrow {
    content: '';
    position: absolute;
    left:100%;
    width: 10px;
    height: 100%;
    background-color: red;
} 

Hope it will help you

Rafael
  • 525
  • 7
  • 20