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();
        }
      }
    }
  }
};
PluginPurposeRegistered how
title / subtitleChart heading and captionBuilt in; imported as Title, SubTitle
legendDataset labels and togglingBuilt in
tooltipHover detailBuilt in
colorsAuto-assigns a paletteImport Colors and register
fillerFills the area under a lineFiller, plus fill: true on the dataset
decimationDrops points when the canvas is smallDecimation plus options.plugins.decimation
legend-click behaviourCustom togglingOverride onClick in the legend options
annotation (external)Reference lines and boxeschartjs-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.label together with labelColor.
  • Never build a custom tooltip with innerHTML and data values. Use textContent or create elements: the tooltip content comes from your data, and data can be attacker-controlled.
  • filter is the right way to hide a zero series from the tooltip without hiding the series from the chart.
  • For a scatter chart, mode: 'nearest' with intersect: true reads much better than index, 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;
    }
  }]
};
GoalMechanismNote
A palette with no configurationColors pluginRegistration is enough
Colour by data valueScriptable backgroundColorRuns per element; keep it cheap
A gradient fillAn inline plugin drawing to the canvasRecompute on resize
Emphasise the hovered serieshover options or an onHover handlerAvoid changing the data
Match a design systemExplicit colours per datasetMost predictable
A colour-blind-safe paletteExplicit colours plus shape variationThe 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.

Chart types and datasets Accessibility and colour choices

Last refreshed 2026-09-18.