SVG Arrows with hover

At Varvet we’re working on a client project that makes heavy use of an SVG canvas. It involves lines connecting a bunch of circles and since directionality is important, the lines all have a little arrow head to indicate direction. The easiest way to do this is with markers: we just slap a "marker-end" style attribute on the SVG element and things will look fine:

Image is no longer available.

This is really nice since the arrow will track the line direction. The arrows in our project are bezier curves so that feature is really handy, no matter how we bend and twist our lines the marker will stick to the end of our line and things are still looking great. If that was all we wanted to do, we’d be done! We have a line from point A to point B and an arrow head indicating its direction.

Hover state

Turns out it’s not that easy. Our lines can be dragged and manipulated, and they change colors to indicate what the user is doing (hovered, clicked, being dragged, state etc). Adding a hover effect is easy enough with CSS, we just add path:hover { stroke:#f00 } to our selectors. But look at what happens (or rather, what DOESN’T happen) to the arrow head when you hover the line:

Image is no longer available.

What! No hover state on markers?

The line lights up but the arrow stays the same color. And you can’t mouseover the arrow head either - expected behavior would be for the line and arrow head to feel like the same object and receive mouseover and mouseout events. Turns out they don’t! As the SVG Spec tells us:

Event attributes and event listeners attached to the contents of a ‘marker’ element are not processed; only the rendering aspects of ‘marker’ elements are processed.

This means if we want to use markers and have the hover state work as expected, we’re going to have to use JS. One way would be to use different markers with pre-defined colors, listen to the mouseover and mouseout events for our line and swap the markers on hover:

Image is no longer available.

That works sort of the way we’d like it and if the hover state was all we needed and we didn’t care about the arrow head being hoverable, it would be enough. But every state you need to track means yet another marker to add to your <defs> and you can’t add transitions to the arrow head, either - you get a straight swap between markers and that’s it (and as an added bonus, Internet Explorer has a bug that prevents it from redrawing a line with a marker at the end of it if you change its "d" property). Not optimal!

Tracking the arrow head yourself

Markers are the quick and easy way to add objects to your line and if your SVG paths are uncomplicated and don’t need state tracked, by all means go ahead and use them! But if you need to add event listeners and states to your paths you should consider drawing them yourself. We get two functions from our SVG path (getTotalLength and getPointAtLength) that make finding the end of a path easy, and then we can just draw the arrow head ourselves. It’s a polygon with three points, with the tip offset slightly after the line (the line will poke through it otherwise). We get the endPoint from JS and drawing it on a left-to-right path is really easy:

Image is no longer available.

The hover works exactly the way we wanted it to. You can hover the entire line, arrow head and all, and we can control all the hover effects with CSS, meaning we can have transitions and an unlimited amount of color states. It’s as easy as adding a class.

Angles, curves and points

It’s a little harder with bezier curves - we have to dust off our trigonometry before we can make the arrow head follow the arrow. The first step is to determine the angle at the end of our line, so we know in which direction the arrow head is supposed to point. Next, we place an imaginary circle on the line’s endpoint and use it to draw our three polygon points, like this:

Image is no longer available.

Finding the angle between two points in JavaScript is done like this:

var angle = Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x);
var angleDeg = angle * 180 / Math.PI;

The angleDeg variable is a conversion from radians to degrees, so we can place our points at their places in a 360 degree circle. This is done in a little helper function with our friends sin and cos. We supply it with a point, an angle in degrees, and an offset from that angle:

var pointOnCircle = function(p, a, d) {
  var newAngle = (a+d) * Math.PI / 180;
  return  (p.x + Math.cos(newAngle) * triangleSize) + ',' +
          (p.y + Math.sin(newAngle) * triangleSize);

…and presto! SVG arrows in any direction, with full CSS control and hover state.

Image is no longer available.