Grow your CSS skills. Land your dream job.

Links with Inline SVG, Staying on Target with Events

Published by Chris Coyier

It's pretty common to use SVG within an anchor link or otherwise "click/tappable thing" on a web page. It's also increasingly common that the SVG is inline <svg>, because it's often nice having the SVG in the DOM since you can style it with CSS and script it with JS and such. But what does that mean for click events?

A link with an SVG icon in it might be like this:

<a href="#0" data-data="something">
  <svg ... >
     <rect ...>
  <svg>
</a>

Now you want to bind a click event to that anchor link. In jQuery:

$("a").on("click", function(event) {
  
   // `this` will always be the <a>
  console.log($(this).data("data"));
  // "something"

});

That will work perfectly fine. Note there is a data-* attribute on the anchor link. That's probably there specifically for JavaScript to access and use. No problem at all how we have it written right now, because within the anonymous function we have bound, this will always be that anchor link, which has that attribute available. Even if you use event delegation and call some function who-knows-where to handle it, this will be that anchor link and you can easily snag that data-* attribute.

But let's say you're going to rock some raw JavaScript event delegation:

document.addEventListener('click', doThing);

function doThing(event) {
  // test for an element match here
}

You don't have an easy reference to your anchor link there. You'll need to test the event.target to see if it even is the anchor link. But in the case of our SVG-in-a-link, what is that target?

It could either be:

  1. The <a>
  2. The <svg>
  3. The <rect>

You might just have to check the tagName of the element that was clicked, and if you know it was a sub-element, move up the chain:

document.addEventListener('click', doThing);

function doThing(event) {
  var el;
  
  // we can check the tag type, and if it's not the <a>, move up.
  if (event.target.tagType == "rect") {
    // move up TWICE
    el = event.target.parentElement.parentElement;
  } else if (event.target.tagType == "svg") {
    // move up ONCE
    el = event.target.parentElement;
  } else {
    el = event.target;
  }
  console.log(el.getAttribute("data-data"));
}

That's pretty nuts though. It's too tied to the HTML and SVG structure. Toss a <g> in there around some <path>s, which is a perfectly fine thing to do for grouping, and it breaks. Or the tagType is path not a rect, or any other DOM difference.

Personally I've been preferring some CSS solutions.

One way is to lay a pseudo element over top the entire anchor element, so that the clicks are guaranteed to be on the anchor element itself, nothing inside it:

a {
  position: relative;
}
a::after {
  content: "";
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

This seems to work pretty well too:

a > svg {
  pointer-events: none;
}

pointer-events typically doesn't work in IE (it does in 11+, but not lower), but it actually does when applied to SVG, in IE 9+, which is the version of IE that supports SVG anyway.

Here's a Pen with the issue demonstrated and the fixes:

See the Pen owyFj by Chris Coyier (@chriscoyier) on CodePen.

Point is

If you're using SVG in a click target, be very careful you're getting the right element in JavaScript, and if you have trouble, consider a CSS cover.


Wanna learn more about SVG?

I have a full course available called Everything You Need to Know about SVG that covers the whole spectrum of SVG from the perspective of a web designer and front end developer.

Comments

  1. This problem with raw event delegation could apply to almost any child element(s) in an anchor, not just SVG. I’ve had similar issues with animated buttons where the pointer might start the mouseDown in one element (say, an icon graphic) but have the mouseUp in a different element (say, text), which means it won’t trigger a proper click event. A full-size pseudo element to cover the entire <a> is a super clever work-around.

  2. David Gilbertson

    Doesn’t the event bubble up inside svg as it does normally through the DOM? In which case your function could start with if (event.target.tagType !== "a") { return; } and eventually, even if the click landed on rect, the event would fire on the a as it bubbles up.

    Perhaps I’m missing something…

    • It might actually. But in the case of event delegation, nothing is bound to the a. You need to check the target to determine if you should do anything.

    • It’d be pretty simple to have a catch-all kind of test with jQuery that isn’t tied to the structure of the SVG (although the pseudo-element solution is probably better)

      if ($(event.target).is("a")) {
          el = $(event.target);
      } else {
          el = $(event.target).closest("a");
      }
      
    • Actually, I just remembered that .closest checks the element it’s called on, too, so that if block isn’t necessary.

  3. Chris

    I’m not sure i understand the problem being solved..

  4. Exactly Nothing found in Latest Firefox.

  5. Jimmy

    I think David is almost there – on at least one occasion what I have done is made this recursive, something like this pseudo code:
    function thisHandler(){
    if (this != reqElementType){
    return thisHandler(this.parentElement);
    }else{
    return [whatever we need to return]
    }

    (Plus a little check to break out if we eventually hit DOCUMENT or WINDOW or whatever.)

  6. Rumpelrausch

    Prototype.js has a comfortable element method: up().

    $(eventObject).up('a') will return the surrounding <a> element even if eventObject is the link itself.

    When using such a central event handler you could tag all event “catching” elements with a pseudo class, e.g. ” evReceiver”:

    $(eventObject).up('.evReceiver')

    You can even use it with an opposite meaning, e.g. ignore all mousemove events inside a tagged controller (a common problem when constructing self-made dropdown elements).

    Of course there’s a similar way to do this with jQuery, but using prototype.js keeps your awareness closer to the underlying DOM, the “everything-is-an-object” policy and prototype extension instead of class/method thinking.

  7. Rumpelrausch

    One more subject to remember:
    Global event handlers can result in performance issues. If you register lots of event handlers for single elements the JS engine will handle the dispatching internally (which is fast). If you do the dispatching yourself it might be harder for the engine to optimize.

    In addition a global event handler fires on many elements you don’t want to handle at all.

    If you can, proceed like this:
    Register event handlers for each and every element you want to handle. Don’t use inline (lambda) functions; Instead create general functions and reference them in your event registering code. This function caching might be obsolete with modern JS engines, but it’s definitely faster with older browsers.

  8. Why not just this?

    function doThing(event) {
        var el = event.target;
        while ( el.tagName != "A" )
            el = el.parentNode;
       console.log(el.getAttribute("data-data"));
    }
    
    • Because the <a> might not be the parentNode, it might be deeper than that.

    • Andrew

      Because the <a> might not be the parentNode, it might be deeper than that.

      The while loop should take care of that.

    • Oh right! I didn’t even see that. Derp. Looks like that would work fine. Any reason on parentNode vs parentElement? I guess it doesn’t matter much in this case.

    • Andrew

      Because “back in my day, we didn’t have no fancy parentElement, we only had parentNode!” haha

  9. **I have something like this on my javascript code:

    var mouseOverHandler = function() {
      console.log("do something..");
    };
    
    var mouseOutHandler = function() {
      console.log("do something else ..");
    };
    
    $("[id^='SVG']").each(function() {                                                                                                                       
      $(this).hover(mouseOverHandler, mouseOutHandler);                                                                                                      
    });
    

    and it works pretty nice for me! ;)

  10. You can also check the clicked element by using event.currentTarget

  11. Well, a little bit related with this, using a SVG inside a anchor, if the link already been visited the transition doesn’t work on webkit based browsers. Anyone had the same problem or know any workaround?

    To test: http://jsfiddle.net/eEfjS/

  12. As Jeremy T said, if you’re using jQuery I’m pretty sure you could just use $(event.target).closest(“a”) to always select the link anchor element. Masking it with pseudo CSS seems a bit hacky to me but it’s very clever.

  13. Michael C.

    In order to get the ::after pseudo-element to work, I had to add position: absolute to it. Also, since I’m applying it to a button with rounded corners, I had to round the pseudo-element’s corners as well to get proper click & hover regions.

    • Michael C.

      Also, I’d just like to add that I’m starting to suspect that Chris is actually a Jedi Master. I encountered this problem on May 1st, and immediately after Star Wars day (May the 4th), he posted the solution. :D

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".