Skip to main content

Tooltips and Interactive Overlays React Charts

Tooltips and interactive overlays transform static charts into data exploration tools. Hover over a bar to see its exact value, click to filter, or drag to zoom. This article teaches you to build custom tooltips, manage hover state, detect nearby data points, and layer overlays (crosshairs, legends, labels) on top of your charts. By the end, you'll have an interactive chart that engages users and reveals hidden insights.

What Are Tooltips and Why They Matter?

A tooltip is a small label that appears on hover, displaying detailed information about a data point. A tooltip for a bar might show "Q2: $67,450" when you hover over it. Interactive overlays extend this: crosshairs follow the mouse, a highlight box emphasizes the nearest point, or a legend updates to show selected categories.

Tooltips reduce cognitive load: viewers don't need to read axis labels to understand data. They also improve accessibility by providing alternative text representations of visual data.

Building a Basic Hover Tooltip

Start with a simple bar chart that shows a tooltip on hover:

import React, { useState } from "react";

const data = [
{ label: "Q1", value: 4500 },
{ label: "Q2", value: 6700 },
{ label: "Q3", value: 5200 },
{ label: "Q4", value: 8100 }
];

export function TooltipBarChart() {
const [hoveredIndex, setHoveredIndex] = useState(null);
const maxValue = Math.max(...data.map((d) => d.value));

return (
<div style={{ padding: "40px", position: "relative" }}>
<svg width="400" height="300" style={{ border: "1px solid #ddd" }}>
{data.map((d, i) => {
const x = i * 90 + 10;
const height = (d.value / maxValue) * 200;
const y = 250 - height;

return (
<g
key={i}
onMouseEnter={() => setHoveredIndex(i)}
onMouseLeave={() => setHoveredIndex(null)}
>
<rect
x={x}
y={y}
width="60"
height={height}
fill={hoveredIndex === i ? "orange" : "steelblue"}
style={{ transition: "fill 0.2s", cursor: "pointer" }}
/>

{/* Tooltip on hover */}
{hoveredIndex === i && (
<g>
<rect
x={x + 5}
y={y - 35}
width="80"
height="30"
fill="rgba(0, 0, 0, 0.8)"
rx="4"
/>
<text
x={x + 45}
y={y - 12}
textAnchor="middle"
fill="white"
fontSize="14"
fontWeight="bold"
>
{d.label}
</text>
<text
x={x + 45}
y={y + 2}
textAnchor="middle"
fill="white"
fontSize="12"
>
${d.value.toLocaleString()}
</text>
</g>
)}
</g>
);
})}
</svg>
</div>
);
}

Hovering a bar changes its color and displays a tooltip above it. The tooltip includes the label and formatted value. This pattern is simple and widely applicable.

Advanced Tooltips: Positioned Overlays with HTML

SVG tooltips are limited; HTML overlays offer more flexibility. Position a tooltip div absolutely over the chart:

import React, { useState, useRef } from "react";

export function AdvancedTooltip({ data }) {
const [tooltip, setTooltip] = useState(null);
const svgRef = useRef(null);

const handleMouseMove = (e) => {
if (!svgRef.current) return;

const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// Find nearest data point
const barWidth = 80;
const barIndex = Math.floor((x - 10) / 90);

if (barIndex >= 0 && barIndex < data.length) {
const d = data[barIndex];
setTooltip({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
label: d.label,
value: d.value
});
}
};

const handleMouseLeave = () => {
setTooltip(null);
};

return (
<div style={{ position: "relative", display: "inline-block" }}>
<svg
ref={svgRef}
width="400"
height="300"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ border: "1px solid #ddd" }}
>
{/* Chart bars */}
{data.map((d, i) => (
<rect
key={i}
x={i * 90 + 10}
y={250}
width="60"
height={100}
fill="steelblue"
/>
))}
</svg>

{/* HTML tooltip overlay */}
{tooltip && (
<div
style={{
position: "absolute",
left: `${tooltip.x + 10}px`,
top: `${tooltip.y - 40}px`,
backgroundColor: "rgba(0, 0, 0, 0.9)",
color: "white",
padding: "8px 12px",
borderRadius: "4px",
fontSize: "12px",
whiteSpace: "nowrap",
zIndex: 1000,
pointerEvents: "none" // don't interfere with mouse tracking
}}
>
<div style={{ fontWeight: "bold" }}>{tooltip.label}</div>
<div>${tooltip.value.toLocaleString()}</div>
</div>
)}
</div>
);
}

HTML tooltips are more flexible than SVG. You can style them with CSS, include images, or render complex React components inside them.

Detecting Nearby Points: Crosshairs and Highlights

For line and scatter plots, it's helpful to highlight the nearest point to the cursor:

import React, { useState, useRef } from "react";
import { scaleLinear } from "d3-scale";

export function CrosshairChart({ data }) {
const [hoveredPoint, setHoveredPoint] = useState(null);
const svgRef = useRef(null);

const margin = { top: 20, right: 20, bottom: 40, left: 60 };
const width = 500;
const height = 300;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

const xScale = scaleLinear()
.domain([0, data.length - 1])
.range([0, innerWidth]);

const yScale = scaleLinear()
.domain([0, Math.max(...data)])
.range([innerHeight, 0]);

const handleMouseMove = (e) => {
if (!svgRef.current) return;

const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left - margin.left;
const y = e.clientY - rect.top - margin.top;

// Find nearest point
let nearest = -1;
let minDist = Infinity;

data.forEach((value, i) => {
const px = xScale(i);
const py = yScale(value);
const dist = Math.sqrt((x - px) ** 2 + (y - py) ** 2);
if (dist < minDist) {
minDist = dist;
nearest = i;
}
});

if (minDist < 50) {
// only show tooltip if within 50px
setHoveredPoint(nearest);
} else {
setHoveredPoint(null);
}
};

return (
<svg
ref={svgRef}
width={width}
height={height}
onMouseMove={handleMouseMove}
onMouseLeave={() => setHoveredPoint(null)}
style={{ border: "1px solid #ddd" }}
>
<g transform={`translate(${margin.left}, ${margin.top})`}>
{/* Grid */}
{yScale.ticks(5).map((tick, i) => (
<line
key={i}
x1="0"
y1={yScale(tick)}
x2={innerWidth}
y2={yScale(tick)}
stroke="#e0e0e0"
strokeDasharray="4"
/>
))}

{/* Line */}
<path
d={`M ${data
.map((d, i) => `${xScale(i)},${yScale(d)}`)
.join(" L ")}`}
stroke="steelblue"
fill="none"
strokeWidth="2"
/>

{/* Points */}
{data.map((d, i) => (
<circle
key={i}
cx={xScale(i)}
cy={yScale(d)}
r={hoveredPoint === i ? 6 : 3}
fill={hoveredPoint === i ? "orange" : "steelblue"}
style={{ transition: "r 0.2s, fill 0.2s" }}
/>
))}

{/* Crosshair */}
{hoveredPoint !== null && (
<g opacity="0.5">
<line
x1={xScale(hoveredPoint)}
y1="0"
x2={xScale(hoveredPoint)}
y2={innerHeight}
stroke="gray"
strokeDasharray="5"
/>
<line
x1="0"
y1={yScale(data[hoveredPoint])}
x2={innerWidth}
y2={yScale(data[hoveredPoint])}
stroke="gray"
strokeDasharray="5"
/>
</g>
)}
</g>

{/* Axes omitted for brevity */}
</svg>
);
}

A crosshair (two dashed lines) appears when the cursor is near a point, helping viewers see exact coordinates on the axes.

Using Recharts Built-In Tooltips

Recharts includes Tooltip components that handle much of this automatically:

import {
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer
} from "recharts";

const data = [
{ month: "Jan", sales: 4000, profit: 2400 },
{ month: "Feb", sales: 3000, profit: 1398 },
{ month: "Mar", sales: 2000, profit: 9800 }
];

// Custom tooltip component
const CustomTooltip = ({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div
style={{
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: "8px 12px",
borderRadius: "4px",
color: "white"
}}
>
{payload.map((entry, index) => (
<div key={index} style={{ color: entry.color }}>
{entry.name}: {entry.value}
</div>
))}
</div>
);
}
return null;
};

export function RechartTooltip() {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Line dataKey="sales" stroke="#8884d8" />
<Line dataKey="profit" stroke="#82ca9d" />
</LineChart>
</ResponsiveContainer>
);
}

Pass a custom component as the content prop. Recharts calls it with active (boolean), payload (array of data points), and label (axis value).

Interactive Overlays: Legends and Filters

Overlay interactive elements like clickable legends:

import React, { useState } from "react";

export function InteractiveLegend({ data }) {
const [visibleSeries, setVisibleSeries] = useState(["sales", "profit"]);

const toggleSeries = (series) => {
setVisibleSeries((prev) =>
prev.includes(series)
? prev.filter((s) => s !== series)
: [...prev, series]
);
};

return (
<div>
<div style={{ display: "flex", gap: "20px", marginBottom: "20px" }}>
{["sales", "profit"].map((series) => (
<label key={series} style={{ cursor: "pointer" }}>
<input
type="checkbox"
checked={visibleSeries.includes(series)}
onChange={() => toggleSeries(series)}
/>
{series}
</label>
))}
</div>

{/* Chart renders only visible series */}
{visibleSeries.length > 0 ? (
<svg width="400" height="300">
{/* render visible series */}
</svg>
) : (
<p>Select at least one series to display.</p>
)}
</div>
);
}

Clicking a checkbox toggles the visibility of a data series. React state controls what renders.

Key Takeaways

  • Tooltips are small labels on hover that display detailed information without cluttering the chart.
  • Use SVG tooltips for simple cases; HTML overlays for flexibility and complex styling.
  • Detect nearby points by calculating distance; only show tooltips within a threshold distance.
  • Crosshairs and highlights guide viewers to axis positions.
  • Recharts Tooltip component handles most work; customize with a content function.
  • Interactive overlays (legends, filters, labels) enhance exploration and engagement.

Frequently Asked Questions

How do I avoid tooltips overlapping with the chart edge?

Calculate tooltip position dynamically. If it would go off-screen, reposition it:

const getTooltipPos = (x, y, tooltipWidth, tooltipHeight) => {
let posX = x;
let posY = y;
if (posX + tooltipWidth > window.innerWidth) {
posX = window.innerWidth - tooltipWidth - 10;
}
if (posY + tooltipHeight > window.innerHeight) {
posY = window.innerHeight - tooltipHeight - 10;
}
return { posX, posY };
};

Can I show multiple tooltips simultaneously?

Yes, track an array of hovered points instead of a single index. Render a tooltip for each.

How do I make tooltips accessible?

Include role="tooltip" on the tooltip div and use aria-hidden if it's purely decorative. For keyboard users, provide an alternative (e.g., clicking a bar toggles a detailed panel).

What's the performance impact of frequent tooltip updates?

Minimal if you're just updating state. If you're recalculating expensive values in onMouseMove, debounce or throttle the handler:

const throttledMouseMove = useMemo(
() => throttle(handleMouseMove, 16), // 60 FPS
[]
);

How do I add a delay before showing a tooltip?

Use setTimeout:

const handleMouseEnter = () => {
const timeoutId = setTimeout(() => setTooltip(true), 500);
return () => clearTimeout(timeoutId);
};

Further Reading