Topic 8.2
Tooltips
In one line
A tooltip shows the exact numbers of one mark on demand. This topic builds one that stays on screen, opens by tap and by keyboard focus, shows missing values honestly, and never holds the only copy of a number.
Think of it like this
The label next to a painting in a museum. The painting stays clean, and the label gives details when you step close. But a label that appears only when you stand on one special floor tile would be missed by a visitor in a wheelchair and by anyone who cannot step. A good tooltip can be reached in more than one way.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- tooltip
- A small box that shows the details of one mark when the reader points at it, taps it or focuses it.
- flip
- Moving the tooltip to the other side of the point when it would stick out.
- clamp
- Forcing a number into a range, here keeping the tooltip's left and top inside the container.
- focus
- The element that receives keyboard input. Tab moves it; a focused dot can show its tooltip.
- direct label
- Text written on the chart next to a mark, so the number is visible without any interaction.
Step by step
01A transparent overlay catches every move
Draw the year's line and put one transparent rectangle over the plot. The rectangle gets every pointermove, so the line's thin hit area (Topic 8.1) stops mattering.
02Write the numbers in an HTML box
Find the nearest day with a bisector (Topic 8.3), put a dot on it and add the tooltip: a div with the date and the rides, beside the dot. It uses textContent, and pointer-events: none so it never steals the pointer.
03Keep it on the screen
Add the flip and the clamp. In the first example the last days of December show it: the tooltip moves to the left of the dot instead of hanging off the right edge.
if (left + w > box.width) left = ux - w - 12; // no room on the right: go left
if (top < 0) top = uy + 12; // no room above: go below
left = Math.max(0, Math.min(left, box.width - w)); // last safety04Open by tap and by focus too
The examples below add the other two ways in. Tap: pointerdown with pointerType === "touch" opens the tooltip and keeps it open. Focus: focus opens it, blur and Escape close it.
Try it yourself
- 1
Prefer the left side
In the first example, change
let left = ux + 12insideplacetolet left = ux - w - 12, and change the lineif (left + w > box.width) left = ux - w - 12;toif (left < 0) left = ux + 12;. The July tooltip now sits left of its point (left: 192instead of 371), December stays on the left, and only the January one flips to the right (left: 56). The rule is the same, mirrored. - 2
Treat 0 as missing, the classic bug
In the third example, change
Number.isFinite(v) ? v.toLocaleStringtov ? v.toLocaleString.0now printsno data, although a day with zero rides is real data, and the string"910"now prints910 rides, although it is not a number.null,undefinedandNaNstill printno data. Truthiness is the wrong test for numbers.
Code & diagrams
The three events are sent by the page, so the output never changes. In the editor, hover the chart for real.
Preview
Console
Nothing logged yet.Expected console output
2025-01-01 | Wed 1 Jan 2025: 1,193 rides | tooltip left: 56 top: 134 | naive right edge: 219 of 640 | inside: true
2025-07-18 | Fri 18 Jul 2025: 4,380 rides | tooltip left: 371 top: 30 | naive right edge: 526 of 640 | inside: true
2025-12-31 | Wed 31 Dec 2025: 910 rides | tooltip left: 449 top: 148 | naive right edge: 799 of 640 | inside: trueTwelve monthly dots are focusable. A line of 365 days would not be: see the build, where one focusable chart uses the arrow keys.
Preview
Console
Nothing logged yet.Expected console output
start | hidden
Tab to Jan | shows Jan: 43,439 rides
Tab to Feb | shows Feb: 44,343 rides
Escape on Feb | hidden
focus leaves | hidden
finger taps Jul | shows Jul: 117,803 rides
finger lifts | shows Jul: 117,803 rides
finger taps Aug | shows Aug: 108,806 rides
finger taps the empty chart | hidden
mouse enters Sep | shows Sep: 92,354 rides
mouse leaves | hidden
marks you can reach with Tab: 12innerHTML turned part of the name into a real element. With data you do not control, that is how markup injection starts; textContent can never do it.
Preview
Console
Nothing logged yet.Expected console output
1193 -> 1,193 rides
0 -> 0 rides
null -> no data
undefined -> no data
NaN -> no data
"910" -> no data
1000000000 -> 1,000,000,000 rides
innerHTML : text shown: "Quay & co" | child elements created: 1
textContent: text shown: "<b>Quay</b> & co" | child elements created: 0
long label: width 236 px (220 plus 16 of padding), and the box grows taller: true
without max-width and wrapping: wider than 500 px: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Measure a hidden tooltip
In the first example, move tip.hidden = false; from before tip.textContent = … to after place(x(d.date), y(d.rides));.
Break #2
Close the tooltip when the finger lifts
In the second example, change the pointerleave line to .on("pointerleave", (event) => { hide(); }), so it no longer checks the pointer type.
Myth vs fact
Myth
The title attribute is a tooltip.
Fact
A native title appears after a delay, cannot be styled, does not appear on touch and is not announced consistently. Use it only as a fallback.
Myth
Tooltips are accessible if they appear on hover.
Fact
Hover is the one input that keyboards, phones and screen readers do not have. Add focus and tap, and put the key numbers on the chart.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
For scaled SVGs, convert with the screen matrix:
new DOMPoint(ux, uy).matrixTransform(svg.getScreenCTM())gives client pixels (the examples use it to dispatch events), then subtract the wrapper'sgetBoundingClientRect()left and top. - ▸
A tooltip is only announced if the mark points to it: the build gives the chart
aria-describedby="rides-tip"while the tooltip is open, and the tooltip hasrole="tooltip". Adivthat merely appears is silent for a screen reader. WCAG 2.2 criterion 1.4.13 (Content on Hover or Focus) is level AA. - ▸
Mouse and pen can hover, touch cannot, so test the handlers with
pointerType !== "touch"for hover and=== "touch"for tap. Thepointerleavethat a lifted finger sends is the classic way to make a tap-tooltip close at once (the second break-it).
Remember this
- 1
The problem this solves: a line of 365 days has 365 numbers and no room to print them. A tooltip prints one on demand: the date, the value, maybe the weather. The chart stays readable and the detail is one move away.
- 2
HTML or SVG? An SVG tooltip (a
<text>on a<rect>) scales with the chart, but SVG text does not wrap, does not size its box by itself, and is clipped at the SVG's edge. An HTML tooltip (a<div>withposition: absoluteinside aposition: relativewrapper) wraps, takes CSS, can hang outside the SVG, and is read by screen readers like any text. Use HTML. If the SVG is scaled, convert chart units to CSS pixels first (multiply bysvg.clientWidth / viewBoxWidth); the examples here draw at 1:1 so the numbers stay simple. - 3
Keep it on screen. Put the tooltip up and to the right of the point. If it would stick out on the right, put it on the left. If it would stick out on the top, put it below. Then clamp to the container as a last safety. You can only do this after measuring it: a tooltip with the
hiddenattribute (display: none) has width 0, so unhide it first, then measure. Give itpointer-events: none, or it sits under the pointer, steals the events and flickers. - 4
Keyboard focus. A tooltip that opens only on hover excludes keyboard users. Marks that carry a tooltip can take focus (
tabindex="0"), open it onfocus, close it onblur, and close on Escape. WCAG 2.2 criterion 1.4.13 asks that such content is dismissible (Escape), hoverable (the pointer can move onto it) and persistent (stays until dismissed). Never give 365 pointstabindex="0": the Tab key would need 365 presses to leave the chart. For dense lines use one focusable chart and arrow keys (the build does this). - 5
Never let a tooltip hold the only copy. Screen readers, printouts, screenshots and phones without hover do not see tooltips. Important numbers (the peak, the latest value, the outage day) get a direct label on the chart, and the whole data gets a table (Topic 15.3). Fill the tooltip with
textContent, never withinnerHTMLand data: a station namedA <b>would become markup. A missing value shows "no data", notNaNand not 0.
Explain it without notes
Why measure the tooltip after unhiding it?
Why textContent and not innerHTML for a tooltip?
Name three ways a reader can reach a tooltip, and what event each uses.
Practice
Write place(ux, uy, w, h, boxW, boxH) as a pure function that returns { left, top } and test it on the points (10, 5), (620, 130) and (320, 130) with a 130 by 40 tooltip in a 640 by 260 box.
Add a direct label "Peak: 4,380" to the line chart so the highest day is readable without any interaction.
Trade-offs
- ↔
An HTML tooltip is flexible but needs unit conversion when the SVG is scaled. An SVG tooltip needs none but cannot wrap text.
Done when you can
I can place a tooltip so it never leaves its container, and say why it must be measured after unhiding.
I can open a tooltip by hover, by tap and by keyboard focus, and close it with Escape.
I can write tooltip text with textContent and show a missing value as no data.
I can name numbers that must also be on the chart or in a table, not only in a tooltip.