Interactive SVG Components

SVGs are an increasingly popular way to display data online. I have found myself using SVGs to create ever more complex graphs, maps, diagrams and even a Go game. As I've added more functionality to the SVGs, I've ended up creating and reusing various components. I've written about them here so other people can use them if they want, and so I can remember how I made each of them.

For information about how to make simple SVGs please try my SVG tutorial.

Button

One of the most basic elements for an interactive image is a button. Arguably any element associated with an onmouseup() event can be considered a button (note that buttons tend to be activate when the mouse button is released, not when it is first pressed down).

To make something that looks a bit like a button, we can draw a rectangle (possibly with rounded corners) and write some text on it. We can then group these elements with the <g> tag and add the onmouseup() event to them both.

<script type="text/ecmascript"> 
<![CDATA[
  function buttonClick(evt)
  {
    alert('Click');
  }
]]></script> 
 
<g onmouseup="buttonClick(evt)">
  <rect x="20" y="1" rx="5" ry="5"
        width="52" height="22"/>
  <text x="25" y="16">Button</text>
</g>

This basic design is far from perfect - for one thing, if it weren't for the label, it might not be obvious that it even is a button.

The main problem is the way the cursor changes when it moves over the text element. To fix this, we set the cursor attribute of the group to 'pointer'. We can also make it more obviously a button by adding a hover effect.

<style> 
  g.button:hover{
    opacity: 0.75;
  }
</style>

<g class="button" cursor="pointer"
   onmouseup="buttonClick(evt)">
  <rect x="20" y="1" rx="5" ry="5"
        width="52" height="22"/>
  <text x="25" y="16">Button</text>
</g>

Other options for the cursor attribute include:

  • default
  • crosshair
  • move
  • text
  • wait
  • help
  • e-resize (resize eastwards, also w-, s-, n-, ne-, nw-, se- and sw-resize)

 

Draggable SVG element

A common form of computer interaction is the ability to move an element on the screen by clicking on it and dragging it. This can achieved relatively easily in an SVG as below. I used this techique a lot for the interactive demos in my SVG tutorial.

Set up

First we start with some objects we want to move and give them the class "draggable", for example:

<rect class="draggable"
      x="30" y="30"
      width="80" height="80"
      fill="blue" />
<rect class="draggable"
      x="160" y="50"
      width="50" height="50"
      fill="green" />

This doesn't do very much, but it means we can style the elements with CSS. For example, it's quite important to let the user know that an object can be dragged, and the most common way to indicate that is to change the cursor:

<style>
    .draggable {
        cursor: move;
    }
</style>

When the mouse is moved over any element with the class 'draggable', the cursor will change to a 'move' cursor (normally a hand or crossed arrows).

Selecting an element

The click-and-drag functionality is split into two obvious events. The first is the click, which is triggered when the left mouse button is pressed down while the cursor is over an object. When this happens, we want to select that object by assigning it to a variable. Perhaps less obviously, we also want to record the position of the mouse so that when it moves, we know by how much it has moved.

To the elements you want to be able to move, add an onmousedown trigger that calls a function selectElement. It should pass the event, evt, so we know which element triggered it and where the mouse was at the time. For example:

<rect class="draggable"
      x="30" y="30"
      width="80" height="80"
      fill="blue"
      transform="matrix(1 0 0 1 0 0)"
      onmousedown="selectElement(evt)"/>

In order to move the element we also need to add a transform matrix. We could just use transform="translate(0 0)", but using a matrix is more versatile. The matrix is currently set at (1 0 0 1 0 0), which is the equivalent of the identity matrix (so has no effect).

Now we add the code to deal with selecting the element.

<script type="text/ecmascript"> <![CDATA[
  var selectedElement = 0;
  var currentX = 0;
  var currentY = 0;
  var currentMatrix = 0;

  function selectElement(evt) {
    selectedElement = evt.target;
    currentX = evt.clientX;
    currentY = evt.clientY;
    currentMatrix = selectedElement.getAttributeNS(null, "transform").slice(7,-1).split(' ');
    
      for(var i=0; i<currentMatrix.length; i++) {
        currentMatrix[i] = parseFloat(currentMatrix[i]);
      }

    selectedElement.setAttributeNS(null, "onmousemove", "moveElement(evt)");
  }
]]> </script>

When we mousedown on the element, we record what element it is and what it's current transformation is (which we save as an array of floats for easy manipulation). We also record the x and y coordinates of the mousedown event because we need to be able to calculate relative mouse movements. Finally, we add an new event trigger for onmousemove, which calls a function, moveElement().

Dragging an element

The function that deals with moving the element looks like this:

function moveElement(evt){
  dx = evt.clientX - currentX;
  dy = evt.clientY - currentY;
  currentMatrix[4] += dx;
  currentMatrix[5] += dy;
  newMatrix = "matrix(" + currentMatrix.join(' ') + ")";
            
  selectedElement.setAttributeNS(null, "transform", newMatrix);
  currentX = evt.clientX;
  currentY = evt.clientY;
}

This function finds the new coordinates of the mouse and how this position differs from the previously recorded position. It then updates the 4th and 5th (counting from 0) numbers in array representing the selected element's transformation, which corresponds to a translation dx units left and dy units down. The array is used to generate a new matrix which is added to the selected element. Finally, the new position of the mouse is recorded.

Deselecting an element

If you view the SVG now, you'll be able to select and element and move it, but you won't be able to 'let go' and select a new one. To deselect we add a new function:

function deselectElement(evt){
  if(selectedElement != 0){
    selectedElement.removeAttributeNS(null, "onmousemove");
    selectedElement.removeAttributeNS(null, "onmouseout");
    selectedElement.removeAttributeNS(null, "onmouseup");
    selectedElement = 0;
  }
}

The function should be fairly self-explanatory, we just remove the onmousemove trigger and set the selectElement variable to 0. It also removes onmouseout and onmouseup triggers, which we haven't yet added. They can be added when the element is selected along with the onmousemove trigger and should call the deselect function, e.g.:

selectedElement.setAttributeNS(null, "onmouseout", "deselectElement(evt)");
selectedElement.setAttributeNS(null, "onmouseup", "deselectElement(evt)");

You can now drag the elements. At the moment if you drag an element to one that is 'above' it in z, then dragging will stop. At a later date I might add a function to move a selected element to the top (i.e. so it is drawn last). To see how this might be done take a look at Moving SVG elements in Z.

 

AttachmentSize
drag_cursor.svg413 bytes
drag_elements.svg2.19 KB

Isometric projection

Isometeric projections are commonly used in technical drawings and used to be used in some computer game graphics. In an isometeric projection the three axes appear 120° from each other and are equally foreshortened. It can be achieved by rotating an object 45° in the plane of the screen and ~35.3° through the horizontal axis (see Wikipedia for more information).

We start with an image, such as this floor plan.

If we rotate this image 45° now, it will move almost off screen as rotations are always centred on the point (0, 0).

Translation

So we first translate our image so the point around which we want to rotate it (probably the centre of the image) is now at the point (0, 0). For example, the centre of this image is at (150, 80), so we translate the room by wrappping it in a group ike so:

<g transform="translate(-150, -80)">
  room shapes...
</g>

Rotation in the plane of the screen

We can now rotate our image. A rotation of 45° in the plane of the screen is easy enough with transform="rotate(45)", but a rotation through the horizontal axis is more tricky. The solution is to use matrices. A rotation in the plane of the screen is a rotation through the z-axis and given by the following 3x3 matrix:

Because SVGs are 2D, they don't bother with the final row of this matrix (although it is important for intermediate steps as we'll see). SVG matrices are given as a single line of parameters in the order (column 1, row 1), (column 1, row 2), (column 2, row 1) and so on. Therefore we can replace transform="rotate(45)" with:

transform="matrix(0.707 0.707 -0.707 0.707 0 0)"

Rotation through the horizontal axis

Rotating something through the horizontal (or x-) axis means effectively tilting the image away from us. Such a rotation is given by the following 3x3 matrix:

In an isometric projection, the angle the camera moves is ~35.3°, however, this assumes that what we're looking at is lying on the floor (i.e. the xz-plane). Our image is "upright", in the xy-plane, so we actually need to rotate by 90°-35.3°. Theta is therefore 54.7°.

If we now multiply Rz(45°) by Rx(54.7°) we get:

Or in SVG terms:

transform="matrix(0.707 0.409 -0.707 0.409 0 -0.816)"

We can now add this to a group that contains the group with the translate transformation.

Translation back to the centre

Finally, we need to move our image back to the centre of the screen. We can do this a few ways. We could leave the image where it is and change the viewBox to be centred on (0, 0):

viewBox="-150, -80, 300, 160"

Alternatively, we could just add another group with a translation back:

<g transform="translate(150, 80)">

You may find that you have to tweak the translation a bit to ensure the corners stay within the bounds of the image.

Finally, if you're feeling very adventurous or want to minimise the size of the SVG and processing require to display it, you can combine the two translation matrices with the rotation matrix. The first translation matrix, T1, is given by:

The second translation matrix, T2, would be the same, but with positive 150 and 80. If we multiply T2 x Riso x T1 we get:

<g transform="matrix(0.707 0.409 -0.707 0.409 100.51 -14.78)">

Note that this transformation will only work well if you want to rotate about the point (150, 80). As you can see from the file sizes below, we've saved ourselves 94 bytes.

 

AttachmentSize
room_plan.svg637 bytes
room_plan_translation.svg675 bytes
room_plan_rotate_without_translate.svg666 bytes
room_plan_isometric.svg794 bytes
room_plan_isometric_matrix.svg700 bytes

Multi-line text box

One feature that I know a lot of people, myself included, would like in SVG is a multi-line text box, so I've made one. You can see the result below and download the file at the bottom of the page. In Chrome, at least, you should see two multi-line blocks of text and two rectangles of empty space. In Firefox or a program like Inkscape you will see two long lines of text. I'm trying to find a simple way to get it to work in Firefox.

The SVG contains two texts element like this:

<text x="5" y="20" onload="create_multiline(evt, 280)">Alice was beginning...</text>
<text x="160" y="155" onload="create_multiline(evt, 200)">So she was considering...</text>

As you can see, both elements call a function called create_multiline() when they load (which is what Firefox doesn't do). The parameters passed are evt, which allows the function to know which element called it, and a number which defines the width of the box.

The createmultiline function does the following:

Split text at spaces to get an array of words

var words = text_element.firstChild.data.split(' ');

Remove the words from the original text element

text_element.firstChild.data = '';

Add a tspan element containing the first word to the text element

var tspan_element = document.createElementNS(svgNS, "tspan");
var text_node = document.createTextNode(words[0]);
tspan_element.appendChild(text_node);
text_element.appendChild(tspan_element);

Add words to the tspan element one by one

for(var i=1; i<words.length; i++) {
    tspan_element.firstChild.data += " " + words[i];

If the width is exceeded...

if (tspan_element.getComputedTextLength() > width) {

Remove the previous word

tspan_element.firstChild.data = tspan_element.firstChild.data.slice(0, len);

Add a new tspan element with that word

var tspan_element = document.createElementNS(svgNS, "tspan");
tspan_element.setAttributeNS(null, "x", start_x);
tspan_element.setAttributeNS(null, "dy", 18);
text_node = document.createTextNode(words[i]);
tspan_element.appendChild(text_node);
text_element.appendChild(tspan_element);

Start adding words one by one again (continue loop).

AttachmentSize
multiline_text.svg2.6 KB

Pan and zoom control

Since SVGs are infinitely scalable, it's useful to add controls to pan and zoom, particularly to maps. Panning and zooming can be achieved by manipulating the viewBox attribute, but that will affect the entire SVG including the controller. For a more targetted effect, you can wrap the elements you want to change in a group with a transform attribute.

In this post, I'll explain now to make a pan and zoom controller that can be easily added to any SVG, to make something like the map below.

The transform attribute

Panning and zooming can be easily achieved using the transform attributes translate and scale respectively.

To pan left by 10 units, you translate everything 10 units right by wrapping it in a group like this:

<g transform="translate(10 0)">
  All the other elements...
</g>

To zoom in by 25%, you scale everything by 1.25 unit by wrapping it in a group like this:

<g transform="scale(1.25)">
  All the other elements...
</g>

However, this will zoom, centred on the top, left corner. Normally we want to zoom centred on the centre of the screen, which means we need to combine scaling with a translation. And when you start combining scaling and translation transformation, things start getting a bit trickier (because you also need to scale previous translations).

Matrix transformations

Using matrices makes transformations a lot simpler in the long run, because they allow us to easily update our transformation. However, if you're not familiar with matrices, then they can be a bit tricky (and I'd recommend watching the first few linear algebra videos at the Khan Academy).

The SVG transform matrix is applied like this:

<g transform="matrix(a b c d e f)">
  All the other elements...
</g>

Where a,b, c, d, e and f form the top 2 lines of a 3x3 matrix. The code treats every coordinate in the group as a vector [x, y, 1] and multiplies it by a matrix like this:

Matrix multiplication

The result is that:

  • the new x-coordinate of each element is ax + cy + e
  • the new y-coordinate of each element is bx + dy + f

For example, transform="matrix(1 0 0 1 10 0) will make each x-coordinate become 1x + 0y + 10 and each y-coordinate become 0x + 1y + 0. In other words, we translate the matrix 10 units to the right.

With transform="matrix(1.25 0 0 1.25 0 0) each x-coordinate becomes 1.25x + 0y + 0 and each y-coordinate becomes 0x + 1.25y + 0. In other words, we scale the matrix by 25%.

Initialising the SVG

Now we can create some functions to pan (translate) and zoom (scale) our map. First wrap all the graphical in a group with and id so we can select it and a transform matrix set as (1 0 0 1 0 0), which is the identity matrix, i.e. all the elements remain the same.

<g id="map-matrix" transform="matrix(1 0 0 1 0 0)">
  All the other elements ...
</g>

Now we create a script element that sets up some variables for the transform matrix, the height and width of the SVG and an array of values for the transformation matrix. We could get the transformation matrix values by parsing the map-matrix element, but it's quicker and easier to just set up an array.

<script type="text/ecmascript">
<![CDATA[
  var transMatrix = [1,0,0,1,0,0];
        
  function init(evt)
  {
    if ( window.svgDocument == null )
    {
      svgDoc = evt.target.ownerDocument;
    }
    mapMatrix = svgDoc.getElementById("map-matrix");
    width  = evt.target.getAttributeNS(null, "width");
    height = evt.target.getAttributeNS(null, "height");
  }
]]>
</script>

At onload="init(evt)" to the svg element so the function is called when the SVG loads and the variables are set.

The pan function

The pan function takes two variables which determine the distance along the x- and y-coordinates to pan. For example, pan(10,0) pans 10 units right and pan(-20,10) pans 20 units left and 10 units down. The function works by adding the translation factor to the last two values in the transformation matrix.

function pan(dx, dy)
{     	
  transMatrix[4] += dx;
  transMatrix[5] += dy;
            
  newMatrix = "matrix(" +  transMatrix.join(' ') + ")";
  mapMatrix.setAttributeNS(null, "transform", newMatrix);
}

The zoom function

The zoom function takes a single parameter, which determines the zoom factor. For example zoom(1.25), makes everything 25% larger, so zooms in, zoom(0.8), makes everything 80% of the original size, so zooms out. The function works by multiplying every value in the matrix by the scaling factor, which not only scales all the elements, but scales any previous translations. It then translates the matrix in such as way as to keep the centre in the centre by adding a factor to the last two values in the transformation matrix.

function zoom(scale)
{
  for (var i=0; i<transMatrix.length; i++)
  {
    transMatrix[i] *= scale;
  }

  transMatrix[4] += (1-scale)*width/2;
  transMatrix[5] += (1-scale)*height/2;
		        
  newMatrix = "matrix(" +  transMatrix.join(' ') + ")";
  mapMatrix.setAttributeNS(null, "transform", newMatrix);
}

The controller

Now we've defined the functions we can create a controller that calls them. Below is a very simple example, but being SVG, you could make something a lot more intricate. The important features are the onclick="pan(50,0)", etc.

<style>
  .compass{
    fill:  #fff;
    stroke:  #000;
    stroke-width:  1.5;
  }
  .button{
    fill:  #225EA8;
    stroke:  #0C2C84;
    stroke-miterlimit:	6;
    stroke-linecap:  round;
  }
  .button:hover{
    stroke-width:  2;
  }
  .plus-minus{
    fill:  #fff;
    pointer-events: none;
  }
</style>

<circle cx="50" cy="50" r="42" fill="white" opacity="0.75"/>
<path class="button" onclick="pan(0,50)"
  d="M50 10 l12 20 a40,70 0 0,0 -24,0z" />
<path class="button" onclick="pan(50,0)"
  d="M10 50 l20 -12 a70,40 0 0,0 0,24z" />
<path class="button" onclick="pan(0,-50)"
  d="M50 90 l12 -20 a40,70 0 0,1 -24,0z" />
<path class="button" onclick="pan(-50,0)"
  d="M90 50 l-20 -12 a70,40 0 0,1 0,24z" />
  
<circle class="compass" cx="50" cy="50" r="20"/>
<circle class="button"  cx="50" cy="41" r="8"
  onclick="zoom(0.8)"/>
<circle class="button"  cx="50" cy="59" r="8"
  onclick="zoom(1.25)"/>

<rect class="plus-minus" x="46" y="39.5"
  width="8" height="3"/>
<rect class="plus-minus" x="46" y="57.5"
  width="8" height="3"/>
<rect class="plus-minus" x="48.5" y="55"
 width="3" height="8"/>

This can then slot into the end of an existing SVG. Make sure it is at the end of the SVG so it floats above the other elements and make sure it's not within the group with the transform attribute, so it remains in place.

AttachmentSize
Australia_pan_zoom.svg6.38 KB

Pan and zoom with text

In response to the previous post I was asked how to make a pan and zoom control that included text. The previous code will work with text, but it will scale the text as you zoom in, which is not necessarily the behaviour you want. Below is the same map of Australia, but now with toponyms. As you zoom in and out the text maintains its position relative the drawing while maintaining its size. This can cause odd effects at extreme scales.

The problem is actually quite tricky and my solution is not particularly elegant. You can see the full code at the bottom of this post. Briefly, it works by having separate groups for the drawing and for the text. The drawing moves as before, but the text elements have to be moved individually when you zoom since their positions relative to one another changes.

for (var i=0; i<nameTranslate.childNodes.length; i++) {
  if (nameTranslate.childNodes[i].nodeName == "text") {
    var child = nameTranslate.childNodes[i];
    var x = parseFloat(child.getAttributeNS(null, "x"));
    var y = parseFloat(child.getAttributeNS(null, "y"));
    var new_x = 250 - (250 - x) * scale;
    var new_y = 150 - (150 - y) * scale;
    child.setAttributeNS(null, "x", new_x);
    child.setAttributeNS(null, "y", new_y);
  }
}

The important lines are 103 and 104 which calculate the new position of the text elements by scaling their distance from the centre of the image. The centre of this image is at (250, 150) - you can changes these values to making the scaling centre on a different point.

Another small point is that the text elements must have the style text-anchor: middle to ensure the centre of the text maintains its position relative to the drawing rather than the beginning.

AttachmentSize
Australia_compass_with_names.svg8.45 KB

Tooltip

After writing a general SVG mouseover post, I thought I'd write a more specific tutorial, describing how to use the mouseover effect to achieve something useful: mouseover text, otherwise known as a tooltip. I've tested the result in Chrome and Firefox. As I've mentioned before Internet Explore (before IE9) doesn't support SVGs at all.

You can view the code for any of SVG examples by right clicking on the image and selecting View Frame Source (or something similar). Alternatively, I've attached all the SVGs at the bottom of this post so you can download them.

1. Getting Started

As usual, we'll start with two passive squares.

The first step is to add the text, which should be added to the end of the SVG, after all the other elements, so it will be drawn on top of them:

<text class="tooltip" id="tooltip"
      x="0" y="0" visibility="hidden">Tooltip</text>

The text reads 'Tooltip' and is currently positioned at coordinates (0,0), though the position is unimportant as the text is hidden. I've given the text element the id "tooltip" which allows us to select it easily; I've also set its class to "tooltip", which will make it easy to style with CSS later.

2. Selecting the Tooltip Element

In order to change the position and visibility of the tooltip <text> element, we have to be able to refer to it. We can refer to it using getElementById but before that we need to be able to refer to the SVG document itself. We do this by calling a function at the beginning of the SVG. The function, init, defines a variable for the SVG itself, which allows us to get elements within it (such as the tooltip text) by their ID. This function goes within a <script> element and within CDATA[ ] brackets. I've written more about SVG scripting here.

<script type="text/ecmascript">
<![CDATA[
  function init(evt)
  {
    if ( window.svgDocument == null )
    {
      svgDocument = evt.target.ownerDocument;
    }
    tooltip = svgDocument.getElementById('tooltip');
  }
]]></script>

Now we add the onload event into the <SVG> element (last line below).

<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="300" height="80"
onload="init(evt)">

3. Displaying the tooltip

Now we have a tooltip variable, we can alter the tooltip text element's properties when we mouseover other objects. Within the CDATA, we define two functions: ShowTooltip to make the tooltip text visible and to position it correctly relative to the mouse, and HideTooltip to hide it again. We use evt.clientX and evt.clientY to get the current position of the cursor within the browser, then set to the tooltip's x, and y positions relative to this. You can play around with the position, unfortunately, it depends a lot on your browser and screen resolution as to how it looks.

function ShowTooltip(evt)
{
  tooltip.setAttributeNS(null,"x",evt.clientX-8);
  tooltip.setAttributeNS(null,"y",evt.clientY-5);
  tooltip.setAttributeNS(null,"visibility","visible");
}

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

We can now add the tooltip effect to any SVG element, such as our two passive boxes. For example:

<rect id="rect1" x="160" y="10"
   width="60" height="60" fill="blue"
   onmousemove="ShowTooltip(evt)"
    onmouseout="HideTooltip()"/>

Note that I've used onmousemove rather than onmouseover so the position of the text is constantly updated.

After adding the function calls to both boxes, the final result should look something like this:

4. Changing the tooltip text

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

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

This method works when you want the mouseover text to display an attribute of elements, but often this won't be the case. For more complex cases, we can change the ShowTooltip function to take a second parameter, the tooltip text:

function ShowTooltip(evt, mouseovertext)
{
  tooltip.setAttributeNS(null,"x",evt.clientX+11);
  tooltip.setAttributeNS(null,"y",evt.clientY+27);
  tooltip.firstChild.data = mouseovertext;
  tooltip.setAttributeNS(null,"visibility","visible");
}

And then change the function calls as required:

onmousemove="ShowTooltip(evt, 'Left box')"
onmousemove="ShowTooltip(evt, 'Right box')"

We might as well style the text now too with a bit of CSS:

<style>
  .tooltip{
    font-size: 12px;
  }
</style>

The result of all this should be something like this:

5. Adding a background

Since it's hard to read the mouseover text on dark backgrounds, such as the blue box, it makes sense to display the text in its own box. We can add a box in the same way we added the text. First we add a <rect> element just before the tooltip <text> element so it's drawn on top of all the elements except the text:

<rect class="tooltip_bg" id="tooltip_bg"
 x="0" y="0" rx="4" ry="4"
 width="52" height="16" visibility="hidden"/>

As before, I've included an class so we can style the box and an id so we can easily select it. The rx and ry values give the rectangle rounded corners.

Then we select the <rect> in the init function:

tooltip_bg = svgDocument.getElementById('tooltip_bg');

And then change its visibility and position in the ShowTooltip and HideTooltip functions:

tooltip_bg.setAttributeNS(null,"x",evt.clientX+8);
tooltip_bg.setAttributeNS(null,"y",evt.clientY+16);
tooltip_bg.setAttributeNS(null,"visibility","visible");
tooltip_bg.setAttributeNS(null,"visibility","hidden");

Finally you can style the box however you like using CSS. For example:

.tooltip_bg{
  fill: white;
  stroke: black;
  stroke-width: 1;
  opacity: 0.85;
}

Which will create a tooltip that looks like this:

7. Changing the tooltip length

As it stands, the width of the box is fixed, so if the tooltip text is too long it will go over the edges. To fix this we calculate the length of the tooltip text with getComputedTextLength() and use this to determine the width of the box:

length = tooltip.getComputedTextLength();
tooltip_bg.setAttributeNS(null,"width",length+8);

If you want multi-line mouseover text and word wrapping then that's more complex and requires multiple text elements. I'll leave that as an exercise for the reader.

AttachmentSize
svg_two_squares2.svg578 bytes
tooltip_colour.svg1.48 KB
tooltip_position.svg1.52 KB
tooltip_basic.svg1.4 KB
tooltip_bg.svg2.01 KB
tooltip_final.svg2.12 KB