Introduction

[May 2, 2018: I've updated this tutorial to fix issues with the viewBox and generally make the code neater.]

The previous tutorial covered basic mouseover effects. This one explains how to use javascript to create a more complex effect: showing a text element when the mouse is hovered over other SVG elements. The code for all the examples on this page can be found here.

The title element

Since writing this tutorial, the title tag has become well supported. However, this tutorial should still be useful if you want a specific style of tooltip. It can also be adapted to create other mouseover effects since it covers several of techniques and principles for making interactive SVG elements. The title tag is also a little annoying to add since you have to create a new element inside the existing one, rather than adding an attribute.

To use the title tag, add it inside the elements like so:

<rect x="40" y="50" width="80" height="100" fill="#007bbf">
    <title>A blue box</title>
</rect>
<rect x="180" y="50" width="80" height="100" fill="#ec008c">
    <title>A pink box</title>
</rect>
blue box A pink box

Then, when you hold your mouse over each element, a tooltip appears after a second or so.

Coding our own

If that's all you want, then the title tag is fine. But what if we wanted to make our own, maybe making the background was a different shape.

The first thing to do is to add a <text> element at the end of your SVG so it will be drawn over the top of the other elements. It doesn't matter what the x and y attributes are, since we will dynamically change them later, and it is current hidden due to the visibility="hidden" attribute. I've given the text element the id "tooltip" which allows us to easily select it.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 200">
    <rect class="tooltip-trigger" x="40" y="50" width="80" height="100" fill="#007bbf" />
    <rect class="tooltip-trigger" x="180" y="50" width="80" height="100" fill="#ec008c" />
    <text id="tooltip" x="10" y="190" visibility="hidden">Tooltip</text>
</svg>

Also add a class to the elements you want to trigger a tooltip. I used "tooltip-trigger" as a class name, but you can use whatever you like, it's just there so we can easily select the elements.

Selecting the tooltip

For this tutorial, I'm going to write the code in a <script> element inside the SVG itself (which means it will still work if I email the SVG to someone). Make sure the script element appears as the last element in the SVG, so the tooltip <text> element exists when the script is run. The code will be the same if you put the JS in a separate file, so long as the SVG is inline, otherwise see my SVG + JS tutorial for how to access the SVG document.

First we need to get the tooltip <text> element. I'm doing this inside an immediately invoked function expression (IIFE). The advantage of this is that it keeps the variables and methods private, so I can have multiple versions running on this page without them interfering with each other (e.g. the tooltip variable won't get overwritten by each script element). It not necessary if you have a single JS file outside of each SVG or have external SVGs embedded into the page, but it's still good practise.

<script type="text/javascript"><![CDATA[
  (function() {
    var tooltip = document.getElementById('tooltip');
  })();
]]></script>

Displaying the tooltip

Next we get the trigger elements with getElementsByClassName and loop through them, adding event handlers for mousemove and mouseout. I've used mousemove rather than mouseover so when we change the position of the tooltip later, it will be constantly updated. If you use mouseover the tooltip to remain stationary after it first appears, which is what happens when you use the <title> element.

var triggers = document.getElementsByClassName('tooltip-trigger');

for (var i = 0; i < triggers.length; i++) {
    triggers[i].addEventListener('mousemove', showTooltip);
    triggers[i].addEventListener('mouseout', hideTooltip);
}

The event handlers added referred to two functions: showTooltip and hideTooltip, which we need to write. These make the tooltip visible or hidden respectively.

function showTooltip(evt) {
  tooltip.setAttributeNS(null, "visibility", "visible");
}

function hideTooltip() {
  tooltip.setAttributeNS(null, "visibility", "hidden");
}

Now if we move the mouse over either box, the tooltip appears at the bottom of the SVG. When we move the mouse off either box, the tooltip disappears.

Tooltip

Positioning the tooltip

Now we want the tooltip to appear next to the mouse. In order for the positioning to work we need to understand the effect of the SVGs viewBox attribute. I've explained in detail how this works here. First we need to get the SVG element which we can do by giving it an id, then using getElementById.

var svg = document.getElementById('tooltip-svg');

Then we update the showTooltip function to get the screen current transform matrix and use this to calculate where the tooltip should be. Again, I explain what getScreenCTM() returns and how to use it here.

function showTooltip(evt) {
	var CTM = svg.getScreenCTM();
	var mouseX = (evt.clientX - CTM.e) / CTM.a;
	var mouseY = (evt.clientY - CTM.f) / CTM.d;
	tooltip.setAttributeNS(null, "x", mouseX + 6 / CTM.a);
	tooltip.setAttributeNS(null, "y", mouseY + 20 / CTM.d);
	tooltip.setAttributeNS(null, "visibility", "visible");
}

I've shifted the position of tooltip by 6 units in the x direction and 20 units in the y direction because I thought that worked well, but you can play around with the numbers. I also added the style dominant-baseline="hanging" to the tooltip text element so the distance is measured from its top. This means there should be a constant distance between the mouse cursor and the tooltip text regardless of the dimensions of the SVG.

Tooltip

Notice that if you move the mouse to the right of the rightmost box, the tooltip is cut off. You can add overflow="visible" to the SVG style to avoid this.

Changing the tooltip text

It's not very useful to display the same text no matter which element is mouseovered. We can change the text by changing firstChild.data for the tooltip element. For example, we can change the mouseover text to display the colour of the square:

Tooltip
tooltip.firstChild.data = evt.target.getAttributeNS(null, "fill");

This method works when you want the mouseover text to display an attribute of elements, but normally this won't be the case. However, we can always add an attribute that contains the text we want to display. Generally when you add attributes contain information we want to use, the attribute should start with data-. For example, I updated the boxes to have data-tooltip-text attributes.

Tooltip
<rect x="40" y="50" width="80" height="100" fill="#007bbf" class="tooltip-trigger" data-tooltip-text="Left box"/>
<rect x="180" y="50" width="80" height="100" fill="#ec008c" class="tooltip-trigger" data-tooltip-text="Right box"/>

Then you can use:

tooltip.firstChild.data = evt.target.getAttributeNS(null, "data-tooltip-text");

Adding the background

Since it can be hard to read mouseover text on dark backgrounds, it makes sense to display the text in its own box. We can make the tooltip more complex by converting it into a <g> element and then adding child elements, such as a <rect> element for a shadow, a <rect> element for a box, and then the tooltip <text> element.

<g id="tooltip" visibility="hidden" >
    <rect x="2" y="2" width="80" height="24" fill="black" opacity="0.4" rx="2" ry="2"/>
    <rect width="80" height="24" fill="white" rx="2" ry="2"/>
    <text x="4" y="6">Tooltip</text>
</g>

The position of the group and the white <rect> element is (0, 0), and the other positions are relative to that. Group elements (<g>) don't have x and y coordinates (or at least they don't change their children's position), so instead we add a translate transform.

var x = (evt.clientX - CTM.e + 6) / CTM.a;
var y = (evt.clientY - CTM.f + 20) / CTM.d;
tooltip.setAttributeNS(null, "transform", "translate(" + x + " " + y + ")");

(Note that I combined adding the (+6, +20) offsets with the calculation of the mouse position, just to make things a bit more efficient).

This way of adding transforms with setAttributeNS is slightly hacky, but it's fine so long are no other transforms are added to the tooltip element (since they will be overwritten). The "correct" method would be to use createSVGTransform() as demonstrated here.

We also need to update the way we change the content of the text element since the tooltip variable no longer directly refers to it. So we can create a new variable to refer to the text inside the first <text> element inside the tooltip group:

var tooltipText = tooltip.getElementsByTagName('text')[0];

Then we change the code that updates the text to use the new variable.

tooltipText.firstChild.data = evt.target.getAttributeNS(null, "data-tooltip-text");

Now our tooltip has a shadow and a box. However, the box and shadow have a fixed width, so we need to make sure it's bigger than the longest text. But then that can look odd with shorter text.

Tooltip

Changing the tooltip length

In order to dynamically change the length of the tooltip boxes we first need to get the boxes, which we can do by getting all the <rect> elements inside the tooltip group.

var tooltipRects = tooltip.getElementsByTagName('rect');

Then, in the showTooltip function we find the length of the tooltip <text> element with getComputedTextLength(), and use this to set the length of the rects.

var length = tooltipText.getComputedTextLength();
for (var i = 0; i < tooltipRects.length; i++) {
    tooltipRects[i].setAttributeNS(null, "width", length + 8);
}

And here's the final SVG with different length boxes. If you want multi-line mouseover text then you could add multiple text elements inside the tooltip group. If you want word-wrapping, then I have some code here that could be used.

The code for all the examples on this page can be found here.

Tooltip