Tooltips using SVG Path

Michael Rovinsky
Feb 24, 2018 · 4 min read

After the Map Pins, let’s try a more interesting SVG path shape: a Tooltip. Here is an example:

The Tooltip shape’s path is defined by 5 parameters: width, height, pointer offset, corner radius, and placement (left, top, right, or bottom).

To make things simple, let’s first build a path for a top-positioned Tooltip without rounded corners. That is what we have:

  1. Move to A: M aX,aY
  2. Line to B: L bX,bY
  3. Horizontal line to C: H cX
  4. Vertical Line to D: V dY
  5. Horizontal line to E: H eX
  6. Vertical Line to F: V fY
  7. Horizontal line to G: H gX
  8. Line to A: L aX,aY
  9. Close the path: z

The point A coordinates are 0, 0. The other points are calculated from width, height, and offset:

bx = -offset
by = -offset // also cy, fy, gy
cx = -width / 2 // also dx
dy = -offset - height // also ey
ex = width / 2 // also fx
gx = offset

… and the whole path will look like this:

function topTooltipPath(width, height, offset) {
const left = -width / 2
const right = width / 2
const top = -offset - height
const bottom = -offset
return `M 0,0
L ${-offset},${bottom}
H ${left}
V ${top}
H ${right}
V ${bottom}
H ${offset}
L 0,0 z`
}

To get rounded corners, we have two options: Arc and Quadratic Bezier curve. An Arc uses 7 parameters (see the Pin example). A QB curve is much simpler: It takes the current point of path (set by previous Move to / Line to) and two additional points: the Vertex and the Target. In SVG, a QB curve is specified by letter Q: Q vX,vY tX,tY.

For the corner C, the starting point of the curve is on the incoming line (H cX) right to C (sX = cX + r; sY = cY). The target point in on the outgoing line (V dY), above C (tX = cX; tY = cY - r). The QB vertex is the C point itself. We just replace … H cX V dY … with: … H (cX + r) Q cX, cY cX, (cY - r) V dY …, and get the C corner rounded (by radius r). Here is an improved function:

function topTooltipPath(width, height, offset, radius) {
const left = -width / 2
const right = width / 2
const top = -offset - height
const bottom = -offset
return `M 0,0
L ${-offset},${bottom}
H ${left + radius}
Q ${left},${bottom} ${left},${bottom - radius}
V ${top + radius}
Q ${left},${top} ${left + radius},${top}
H ${right - radius}
Q ${right},${top} ${right},${top + radius}
V ${bottom - radius}
Q ${right},${bottom} ${right - radius},${bottom}
H ${offset}
L 0,0 z`
}

To make a bottom-positioned tooltip, we just invert all the Y coordinates in the path:

function bottomTooltipPath(width, height, offset, radius) {
const left = -width / 2
const right = width / 2
const bottom = offset + height
const top = offset
return `M 0,0
L ${-offset},${top}
H ${left + radius}
Q ${left},${top} ${left},${top + radius}
V ${bottom - radius}
Q ${left},${bottom} ${left + radius},${bottom}
H ${right - radius}
Q ${right},${bottom} ${right},${bottom - radius}
V ${top + radius}
Q ${right},${top} ${right - radius},${top}
H ${offset}
L 0,0 z`
}

The same is true for the X coordinates of the left and right-positioned tooltips:

function leftTooltipPath(width, height, offset, radius) {
const left = -offset - width
const right = -offset
const top = -height / 2
const bottom = height / 2
return `M 0,0
L ${right},${-offset}
V ${top + radius}
Q ${right},${top} ${right - radius},${top}
H ${left + radius}
Q ${left},${top} ${left},${top + radius}
V ${bottom - radius}
Q ${left},${bottom} ${left + radius},${bottom}
H ${right - radius}
Q ${right},${bottom} ${right},${bottom - radius}
V ${offset}
L 0,0 z`
}
function rightTooltipPath(width, height, offset, radius) {
const left = offset
const right = offset + width
const top = -height / 2
const bottom = height / 2
return `M 0,0
L ${left},${-offset}
V ${top + radius}
Q ${left},${top} ${left + radius},${top}
H ${right - radius}
Q ${right},${top} ${right},${top + radius}
V ${bottom - radius}
Q ${right},${bottom} ${right - radius},${bottom}
H ${left + radius}
Q ${left},${bottom} ${left},${bottom - radius}
V ${offset}
L 0,0 z`
}

Here is a function that does it all:

function tooltipPath(width, height, offset, radius, position) {
let left, top, right, bottom
switch(position) {
case 'top':
return topTooltipPath(width, height, offset, radius)
case 'left':
return leftTooltipPath(width, height, offset, radius)
case 'right':
return rightTooltipPath(width, height, offset, radius)
case 'bottom':
return bottomTooltipPath(width, height, offset, radius)
}
}

For those valuing the JavaScript elegance:

class Tooltip {
topTooltipPath(width, height, offset, radius) {...}
leftTooltipPath(width, height, offset, radius) {...}
rightTooltipPath(width, height, offset, radius) {...}
bottomTooltipPath(width, height, offset, radius) {...}
tooltipPath(width, height, offset, radius, position) {
const method = `${position}TooltipPath`
return this[method](width, height, offset, radius)
}
}

Now a final demo with movable objects and their tooltips. A tooltip’s appearance depends on the object’s position:

Have a nice day!

Michael Rovinsky

Written by

Welldone Software

The leading full-stack consultancy. Creating amazing frontends and rock-solid backends using top notch technologies and practices. Visit us at https://welldone.software.

More from Welldone Software

More from Welldone Software

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade