Radiant Charts ships a fully-featured, portal-based tooltip system that works across all 50+ chart types. Tooltips render in the HTML DOM via React Portals (never clipped by overflow:hidden), while crosshairs and snap indicators render natively on the canvas. The system is headless — use the built-in glassmorphism UI, override it with formatters, or replace it entirely with a custom render function.
The tooltip system is split into two rendering spaces to avoid the most common charting library pitfalls:
document.body. This means it can never be clipped by overflow: hidden on the chart container or any parent element.A framework-agnostic TooltipStore bridges the two: the canvas engine writes state (active entries, anchor position) and React components subscribe via useSyncExternalStore. Each chart instance gets its own store — multiple charts on the same page never interfere.
Add the tooltip and crosshair keys to your RadiantChartOptions:
import RadiantChart from 'radiant-charts';
<RadiantChart
options={{
data: [...],
series: [{ type: 'bar', xKey: 'month', yKey: 'revenue' }],
crosshair: { x: true },
tooltip: {
shared: true,
sticky: true,
positionMode: 'snap-to-data',
delay: { show: 0, hide: 100 },
},
}}
width="100%"
height={400}
/>Use the <Tooltip> config component as a child of <Chart>:
import { Chart, Bar, Line, Tooltip, XAxis, Legend } from 'radiant-charts';
<Chart data={data} crosshair={{ x: true }}>
<Tooltip mode="shared" sticky snap />
<XAxis />
<Legend position="bottom" />
<Bar xKey="month" yKey="sales" fill="#6366f1" />
<Line xKey="month" yKey="target" stroke="#f59e0b" />
</Chart><Tooltip> component renders nothing visible — it's a configuration marker, just like <XAxis> or <Legend>. Its props are merged into the chart's tooltip options automatically.In shared mode, hovering any point on the X axis gathers all series values at that X position into a single tooltip. This is the natural choice for line, area, and bar charts where comparing across series is the primary interaction. Each series gets a color swatch and its own value row.
For stacked bar and area charts, the tooltip automatically calculates each segment's percentage of the stack total and displays it alongside the absolute value.
In item mode, only the single hovered element is shown. This is the default for non-cartesian chart types (pie, treemap, gauge, sankey, etc.) and is recommended for scatter plots where aggregation by X value doesn't make semantic sense.
Set tooltip.mode: "item" or tooltip.shared: false.
The tooltip anchors at the resolved data point's pixel position. It stays locked to the data even as the cursor wobbles slightly. Collision detection flips the tooltip to the opposite side if it would overflow the viewport, then shifts it along the cross-axis to keep it fully visible.
The tooltip tracks the raw cursor position (clientX/clientY) instead of snapping to data points. Data resolution still happens — the tooltip content always shows the nearest data — but the floating box moves with the mouse. Set tooltip.positionMode: "follow-pointer".
The tooltip renders at a fixed corner of the chart container and does not move while active. Useful for dashboards where tooltip movement would be distracting. Set tooltip.positionMode: "fixed".
Fine-tune the distance between the anchor and the tooltip box with tooltip.offset. The y value controls the main axis distance (default: 12px), and x shifts the tooltip along the cross-axis (default: 0).
Crosshairs are canvas-native scene graph elements that render in the crosshairGroup layer, between the series and the watermark. They are configured via the top-level crosshair option (not inside tooltip).
A dashed vertical line from the top to the bottom of the plot area. For BandScale axes (bar, line, area), it snaps to the nearest category center. Set crosshair: { x: true }.
A dashed horizontal line tracking the cursor's Y position. Set crosshair: { y: true }.
When the vertical crosshair is enabled on a BandScale chart, a semi-transparent rectangle highlights the full band behind the active category. This gives clear visual feedback about which category is being inspected.
In shared mode, each series gets a filled circle (snap dot) rendered at its data point for the active X value. Each dot uses the series color with a white stroke halo for contrast. These appear automatically when the shared tooltip is active.
Override how values are displayed with tooltip.valueFormatter. The function receives the raw value and the full TooltipDatumEntry, so you can format differently per series:
tooltip: {
valueFormatter: (value, entry) => {
if (entry.seriesId === 'revenue') return '$' + value.toLocaleString();
if (entry.seriesId === 'convRate') return value.toFixed(1) + '%';
return String(value);
}
}Customize the series name labels with tooltip.labelFormatter. By default, underscores are replaced with spaces and words are title-cased.
In shared mode, the tooltip header shows the X-axis value. Use tooltip.headerFormatter to format it (e.g., converting a date string to a human-readable format).
Control the order of entries in the shared tooltip with tooltip.sortEntries. Use "desc" for highest-value-first, "asc" for lowest-first, or pass a custom comparator function.
When tooltip.sticky: true, the tooltip system supports click-to-pin:
pointer-events: none to pointer-events: auto, becoming fully interactive.Pinned tooltips are ideal for dashboards where users need to inspect a specific data point, copy values, or interact with embedded controls (buttons, links) inside a custom tooltip render function.
When the chart container is focused (click the chart or tab to it), the following keyboard shortcuts are available:
| Key | Action |
|---|---|
Arrow Right / Left | Move to the next / previous data point on the X axis |
Enter | Pin the tooltip at the current data point |
Escape | Unpin and close the tooltip |
Home | Jump to the first data point |
End | Jump to the last data point |
The tooltip system is designed with accessibility as a first-class concern:
role="tooltip" and aria-live="polite" for screen reader announcements.tabindex="0" so it can be focused for keyboard navigation.<table> with className="sr-only") is rendered in every chart as the canonical accessible data representation.prefers-reduced-motion.On touch devices, the tooltip system maps touch events to the same handlers used by mouse events:
touchstart resolves the tooltip at the touch point (equivalent to mousemove).touchend pins the tooltip (equivalent to click) — since there's no hover state on touch, a tap reveals and pins in one gesture.touchmove updates the tooltip as the finger moves across the chart.Replace the entire tooltip UI by passing a render function to the tooltip options. It receives the full TooltipState object and should return a React node:
tooltip: {
render: (state) => (
<div className="my-custom-tooltip">
<h4>{state.sharedLabel}</h4>
{state.entries.map(e => (
<div key={e.seriesId}>
<span style={{ color: e.color }}>{e.title}</span>: {e.yValue}
</div>
))}
</div>
)
}For maximum control, use the useChartTooltip() hook to build a completely custom tooltip component from scratch. The hook must be used inside a <RadiantChart> or <Chart> component tree:
import { useChartTooltip } from 'radiant-charts';
function MyTooltip() {
const { active, entries, anchorX, anchorY, pin, unpin } = useChartTooltip();
if (!active) return null;
return (
<div style={{ position: 'fixed', left: anchorX + 16, top: anchorY - 8 }}>
{entries.map(e => (
<div key={e.seriesId}>
{e.title}: {e.yValue}
<button onClick={() => alert(`Drill into ${e.title}`)}>
Details
</button>
</div>
))}
</div>
);
}useChartTooltip(), set tooltip.enabled: false to suppress the built-in tooltip UI and avoid rendering two tooltips simultaneously.Passed via options.tooltip (imperative) or <Tooltip ...props /> (declarative).
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or suppress the tooltip entirely. |
trigger | 'hover' | 'click' | 'none' | 'hover' | What user interaction activates the tooltip. |
mode | 'shared' | 'item' | 'shared' | Shared shows all series at the same X value. Item shows only the hovered point. |
positionMode | 'follow-pointer' | 'snap-to-data' | 'fixed' | 'snap-to-data' | How the tooltip box is positioned relative to the data. |
shared | boolean | true | Shorthand for mode: "shared". Provided for convenience. |
snap | boolean | true | Snap the crosshair and anchor to the nearest data point. |
sticky | boolean | false | Allow click-to-pin. Pinned tooltips become interactive (pointer-events: auto). |
delay.show | number | 0 | Milliseconds to wait before showing the tooltip after hover. |
delay.hide | number | 0 | Milliseconds to wait before hiding the tooltip after the cursor leaves. |
offset.x | number | 0 | Horizontal offset from the anchor point in pixels. |
offset.y | number | 12 | Vertical offset from the anchor point in pixels. |
crosshair.x | boolean | CrosshairStyle | false | Show a vertical crosshair line. Pass an object to customize stroke, width, dash. |
crosshair.y | boolean | CrosshairStyle | false | Show a horizontal crosshair line. |
crosshair.spikes | boolean | false | Show axis-level labels at the crosshair intersection with each axis. |
valueFormatter | (value, entry) => string | auto | Custom formatter for the data value. Receives the raw value and the full TooltipDatumEntry. |
labelFormatter | (label, entry) => string | Title Case | Custom formatter for series labels (replaces underscores, title-cases). |
headerFormatter | (label, entries) => string | identity | Custom formatter for the shared tooltip header (the X-axis label). |
sortEntries | 'asc' | 'desc' | comparator | series order | Sort entries in the shared tooltip. "desc" sorts by value highest-first. |
maxPinned | number | 3 | Maximum simultaneously pinned tooltips in comparison mode (Ctrl+Click). |
render | (state: TooltipState) => ReactNode | undefined | Completely replace the default tooltip UI. Receives the full tooltip state. |
When crosshair.x or crosshair.y is an object instead of a boolean, it accepts these properties:
| Property | Type | Default | Description |
|---|---|---|---|
stroke | string | theme.axisColor | Line color. |
strokeWidth | number | 1 | Line width in pixels. |
lineDash | number[] | [4, 4] | Dash pattern. E.g., [8, 4] for longer dashes. |
visible | boolean | true | Show or hide this specific crosshair line. |
Each entry in the tooltip's entries array has this shape. Available in formatters, custom render functions, and the useChartTooltip() hook.
| Property | Type | Description |
|---|---|---|
seriesIndex | number | Zero-based index of the series in the options.series array. |
seriesId | string | Unique ID for the series (typically the yKey or angleKey). |
seriesType | string | The chart type string (e.g., "bar", "line", "pie"). |
title | string | Display name for the series (from series.title or yKey). |
color | string | Resolved color for the series (fill or palette color). |
datum | Record<string, any> | The full data row object from the original data array. |
dataIndex | number | Index of the data row in the options.data array. |
xValue | any | The resolved X-axis value for this point. |
yValue | any | The resolved Y-axis value for this point. |
pixelX | number | X pixel coordinate (chart-local) of the data point. |
pixelY | number | Y pixel coordinate (chart-local) of the data point. |
percentage | number | undefined | Percentage of total, populated for stacked/pie series. |
The useChartTooltip() hook returns a ChartTooltipContext object with the following fields:
| Property | Type | Description |
|---|---|---|
active | boolean | Whether the tooltip is currently visible. |
pinned | boolean | Whether the tooltip is pinned (sticky mode). |
entries | TooltipDatumEntry[] | Array of data entries for the active tooltip. |
sharedLabel | string | null | The shared X-axis label (in shared mode). |
pointerX / pointerY | number | Raw cursor position in viewport coordinates. |
anchorX / anchorY | number | Snapped data point position in viewport coordinates. |
localX / localY | number | Cursor position relative to the chart container. |
pin() | () => void | Programmatically pin the tooltip at its current position. |
unpin() | () => void | Unpin the tooltip. |
hide() | () => void | Force-hide the tooltip regardless of pin state. |
Tooltips snap to the nearest BandScale category center. In shared mode, all series values at that X position are gathered. Stacked charts include automatic percentage computation. Band highlighting fills the category's column.
Uses 2D nearest-neighbor resolution in pixel space. The hit radius scales with the marker size (minimum 12px). Recommended to use tooltip.mode: "item" since X-axis aggregation rarely makes sense for scatter data.
Non-cartesian charts use the canvas scene graph's built-in hit testing. Hovering a pie slice shows the slice's label, absolute value, and percentage of total. No crosshair lines are drawn — instead, series focus dimming highlights the active slice.
Item-mode tooltips show Open, High, Low, Close as labeled rows. If SMA/EMA indicators are active, their values are included in the shared tooltip entries. The vertical crosshair snaps to candle centers.
Hit testing via the scene graph identifies the specific node being hovered. The tooltip shows the node's label, value, and its percentage of the parent node.
Node hover shows the node label and aggregated flow value. Link hover shows the source, target, and flow value. The tooltip anchors at the cursor position since network layouts have no axis system.
Boxplot tooltips show min, Q1, median, Q3, and max. Violin tooltips show distribution statistics. Histogram tooltips show the bin range and count.
Region hover shows the region name and data value. The tooltip uses scene-graph hit testing on the rendered map paths.
document.body, getBoundingClientRect) is gated behind typeof window !== 'undefined' checks. The portal renders null during SSR.TooltipStore. No global state, no cross-contamination.TooltipStore.destroy() removes the portal, cancels timers, and unsubscribes all listeners on unmount.transition: opacity 0.12s ease). Position changes in snap mode use a short transform transition for smooth gliding. All transitions are suppressed when prefers-reduced-motion: reduce is active.