Options, built-in plugins and tooltips
Configure title, legend and tooltips, use the colors and decimation plugins, colour points by threshold, and write tooltip callbacks that read well.
The option groups that matter
const options = {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, // one tooltip across the axis
animation: { duration: 400, easing: 'easeOutQuart' },
plugins: {
title: { display: true, text: 'Revenue by quarter', align: 'start', padding: { bottom: 16 } },
subtitle: { display: true, text: 'Consolidated, net of refunds', color: '#64748b' },
legend: {
position: 'bottom',
align: 'start',
labels: { usePointStyle: true, pointStyle: 'circle', boxWidth: 8, padding: 16 }
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(15, 23, 42, 0.92)',
padding: 12,
cornerRadius: 8,
displayColors: true,
callbacks: {
title: (items) => items[0]?.label ?? '',
label: (item) => ' ' + item.dataset.label + ': ' + Number(item.parsed.y).toLocaleString(),
footer: (items) => {
const total = items.reduce((sum, item) => sum + Number(item.parsed.y || 0), 0);
return 'Total ' + total.toLocaleString();
}
}
}
}
};| Plugin | Purpose | Registered how |
|---|---|---|
title / subtitle | Chart heading and caption | Built in; imported as Title, SubTitle |
legend | Dataset labels and toggling | Built in |
tooltip | Hover detail | Built in |
colors | Auto-assigns a palette | Import Colors and register |
filler | Fills the area under a line | Filler, plus fill: true on the dataset |
decimation | Drops points when the canvas is small | Decimation plus options.plugins.decimation |
legend-click behaviour | Custom toggling | Override onClick in the legend options |
annotation (external) | Reference lines and boxes | chartjs-plugin-annotation, separately installed |
💡
Plugins registered globally apply to every chart on the page. When only one chart needs the decimation plugin, pass it in the chart's own
plugins array instead of calling Chart.register, so the behaviour is visible where it is used.Tooltip callbacks and custom content
const tooltipOptions = {
plugins: {
tooltip: {
// 'index' shows every dataset at the hovered x position: right for
// a time series. 'point'/'nearest' is right for a scatter.
mode: 'index',
intersect: false,
position: 'nearest',
caretSize: 6,
titleFont: { weight: '600', size: 13 },
bodyFont: { size: 12 },
bodySpacing: 6,
boxPadding: 4,
callbacks: {
title: (items) => {
const raw = items[0]?.raw;
// With a time scale item.label is already formatted; with custom
// data read the raw object instead.
return raw?.when ? new Date(raw.when).toLocaleDateString() : items[0].label;
},
label: (item) => {
const value = Number(item.parsed.y);
const suffix = item.dataset.unit ?? '';
return ' ' + item.dataset.label + ': ' + value.toLocaleString() + suffix;
},
// Colour the caret and the title by the dataset colour
labelColor: (item) => ({
borderColor: item.dataset.borderColor,
backgroundColor: item.dataset.borderColor,
borderWidth: 2,
borderRadius: 2
}),
// Filter which items appear at all
filter: (item) => Number(item.parsed.y) !== 0,
// sort, reverse or otherwise reorder the body lines
footer: (items) => {
if (items.length < 2) return '';
const sum = items.reduce((acc, item) => acc + Number(item.parsed.y || 0), 0);
return 'Sum ' + sum.toLocaleString();
}
}
}
}
};// Fully custom tooltip markup: the external handler builds your own element.
const externalTooltip = {
id: 'externalTooltip',
afterDraw(chart) {
if (!chart.tooltip) return;
},
// Chart.js 4 uses the 'external' option, not the old custom() hook.
beforeTooltipDraw() {}
};
const chart = new Chart(canvas, {
type: 'bar',
data,
options: {
plugins: {
tooltip: {
enabled: false, // turn off the built-in element
external: ({ chart, tooltip }) => {
let el = document.getElementById('chart-tooltip');
if (!el) {
el = document.createElement('div');
el.id = 'chart-tooltip';
el.style.cssText = 'position:absolute;pointer-events:none;background:#0f172a;color:#f8fafc;' +
'padding:8px 12px;border-radius:8px;font:12px system-ui;transition:opacity .15s';
chart.canvas.parentNode.appendChild(el);
}
if (tooltip.opacity === 0) { el.style.opacity = '0'; return; }
el.style.opacity = '1';
el.style.left = tooltip.caretX + 'px';
el.style.top = tooltip.caretY + 'px';
// textContent, not innerHTML: never inject data as markup
el.textContent = tooltip.title.join(' ') + ' ' +
tooltip.body.map((b) => b.lines.join(' ')).join(' ');
}
}
}
}
});- The callbacks return plain strings by default. To style part of a line, return a string with a marker and configure
tooltip.callbacks.labeltogether withlabelColor. - Never build a custom tooltip with
innerHTMLand data values. UsetextContentor create elements: the tooltip content comes from your data, and data can be attacker-controlled. filteris the right way to hide a zero series from the tooltip without hiding the series from the chart.- For a scatter chart,
mode: 'nearest'withintersect: truereads much better thanindex, because neighbouring points are rarely at the same x.
Colouring by value and the colors plugin
import { Chart, Colors } from './register.js';
// The colors plugin assigns a palette automatically when no colour is set.
Chart.register(Colors);
// Colouring points by threshold: scriptable options run per element.
const thresholdChart = {
type: 'bar',
data: {
labels: months,
datasets: [{
label: 'Uptime',
data: uptimes,
backgroundColor: (context) => {
const value = context.parsed?.y ?? context.raw;
if (value >= 99.9) return 'rgba(16, 185, 129, 0.85)';
if (value >= 99.0) return 'rgba(245, 158, 11, 0.85)';
return 'rgba(239, 68, 68, 0.85)';
},
borderColor: 'rgba(15, 23, 42, 0.1)',
borderWidth: 1
}]
},
options: {
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (item) => ' ' + item.parsed.y.toFixed(2) + '% uptime'
}
}
},
scales: { y: { min: 98, max: 100.2 } }
}
};
// Scriptable options also receive the dataset and the chart, which makes
// gradients possible without any plugin.
const gradientFill = {
type: 'line',
data: { labels: months, datasets: [{ label: 'Revenue', data: values, fill: true }] },
options: {
scales: { y: { beginAtZero: true } }
},
plugins: [{
id: 'gradient',
beforeDatasetsDraw(chart) {
const { ctx, chartArea } = chart;
if (!chartArea) return;
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, 'rgba(109, 40, 217, 0.35)');
gradient.addColorStop(1, 'rgba(109, 40, 217, 0.02)');
chart.data.datasets[0].backgroundColor = gradient;
}
}]
};| Goal | Mechanism | Note |
|---|---|---|
| A palette with no configuration | Colors plugin | Registration is enough |
| Colour by data value | Scriptable backgroundColor | Runs per element; keep it cheap |
| A gradient fill | An inline plugin drawing to the canvas | Recompute on resize |
| Emphasise the hovered series | hover options or an onHover handler | Avoid changing the data |
| Match a design system | Explicit colours per dataset | Most predictable |
| A colour-blind-safe palette | Explicit colours plus shape variation | The colors plugin's palette is not designed for it |
FAQ
Why does my tooltip show the wrong label?
With a
time scale, item.label is a formatted date; with custom data shapes it is the raw x value. Read item.raw when you need the original object, and check interaction.mode matches the chart type.How do I disable the tooltip for one chart?
Set
options.plugins.tooltip.enabled = false. To remove tooltip behaviour without unregistering the plugin globally — which would affect every chart on the page — use the per-chart plugins array and simply omit it.Related
Chart types and datasets Accessibility and colour choices
Last refreshed 2026-09-18.