# Content and styling

> Your own text with format, your own markup or a framework's template with render, numbers, roles, positions, live charts and the custom properties of the tooltip.

A tooltip shows up to three parts by default: a title, the series' name and the value. The title is the value's `meta` when it's a string or a number, otherwise its label; a part that's empty or repeats an earlier one is left out. This page changes what a tooltip says, what it looks like and where it shows.

## Your own text

`format` gets the hovered value and returns its tooltip's text. Line breaks show as line breaks. `null` or an empty string means no tooltip for that value, and `undefined` keeps the default text:

**Text with format** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import '@chartist-js/tooltips/styles.css';
import { barChart } from 'chartist';
import { tooltips } from '@chartist-js/tooltips';

const orders = new Intl.NumberFormat('en-US');

barChart(
  '#chart',
  {
    labels: ['Q1', 'Q2', 'Q3', 'Q4'],
    series: [{ name: 'Orders', data: [1830, 2410, 0, 2950] }]
  },
  {
    plugins: [
      tooltips({
        // Text with a line break; no tooltip for a quarter without orders.
        format: item =>
          item.value === 0
            ? null
            : `${item.label}\n${orders.format(Number(item.value))} orders`
      })
    ]
  }
);
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

The item `format` gets has the value's `role`, `seriesIndex` and `valueIndex`, its `value` (a number, or `{ x, y }` when it has both, as in a scatter chart), its `meta`, the `series` and its `seriesName`, its `label` and its `element`. The text is also the focused value's accessible name, so write it to be read out too.

For numbers, the default text is often enough: `numberFormat` takes `Intl.NumberFormat` options, in the reader's locale.

```ts
import { barChart } from 'chartist';
import { tooltips } from '@chartist-js/tooltips';

barChart(
  '#chart',
  { labels: ['Q1', 'Q2', 'Q3'], series: [[1250, 1830, 1470]] },
  {
    plugins: [
      tooltips({ numberFormat: { style: 'currency', currency: 'CHF' } })
    ]
  }
);
```

## Your own markup

The default text is text only: data, `meta` and `format` can't put markup into the page. For markup, `render` fills the tooltip yourself. It gets the tooltip's content element, emptied for each value, the hovered item and the chart's data, so a tooltip can compare a value with its neighbours:

**Markup with render** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import '@chartist-js/tooltips/styles.css';
import './style.css';
import { lineChart } from 'chartist';
import { tooltips } from '@chartist-js/tooltips';

lineChart(
  '#chart',
  {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    series: [{ name: 'Visitors', data: [1200, 1500, 1350, 1600, 1100] }]
  },
  {
    low: 0,
    plugins: [
      tooltips({
        // Your own markup: the value, and the change since the day before,
        // read from the chart's data.
        render(content, item, data) {
          const values = data.series[item.seriesIndex ?? 0].values as number[];
          const value = Number(item.value);
          const before = values[(item.valueIndex ?? 0) - 1];
          const title = document.createElement('strong');
          title.textContent = `${item.label}: ${value} visitors`;
          content.append(title);
          if (before !== undefined) {
            const change = Math.round(((value - before) / before) * 100);
            const trend = document.createElement('span');
            trend.className = change < 0 ? 'down' : 'up';
            trend.textContent = `${change < 0 ? '' : '+'}${change}% since ${
              data.labels[(item.valueIndex ?? 0) - 1]
            }`;
            content.append(trend);
          }
        }
      })
    ]
  }
);
```

`src/style.css`:

```css
.ct-tooltip strong,
.ct-tooltip span {
  display: block;
}

.ct-tooltip .up {
  color: light-dark(#6ee7b7, #047857);
}

.ct-tooltip .down {
  color: light-dark(#fca5a5, #b91c1c);
}
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

`render` also takes a framework's template: mount a component into the content element, and return a function that unmounts it. The plugin calls it before the next value's `render`, when the tooltip hides and on `destroy()`. Content a framework fills in later is fine: the tooltip is placed again when its content changes size.

```ts nocheck
tooltips({
  render(content, item) {
    // Angular: a dynamic component in the tooltip.
    const ref = createComponent(ValueTooltip, {
      environmentInjector,
      hostElement: content
    });
    ref.setInput('item', item);
    appRef.attachView(ref.hostView);
    return () => ref.destroy();
  }
});
```

With `render`, `format` still decides which values have a tooltip and gives their accessible name; without `format`, the name is the content's text. What `render` does is your code: setting an HTML string with `innerHTML` needs your own Trusted Types policy on a page that requires them.

## Which values, and where

`roles` picks the nodes that get tooltips: `point`, `bar` and `slice` by default. Any role works, including the roles of other layers and plugins: `roles: ['line']` gives each series of a line chart one tooltip, with its name, and a target line's tooltip shows its label and value. For what the default text doesn't know, `item.context` is the node's data context, so `format` can describe a trend line by its `slope`, and by its `correlation` how well it fits the values (squared, the R² spreadsheets show):

```ts
import { lineChart } from 'chartist';
import { trendLine } from '@chartist-js/essentials';
import { tooltips } from '@chartist-js/tooltips';

lineChart(
  '#chart',
  { series: [{ name: 'Visitors', data: [12, 15, 14, 18, 21] }] },
  {
    layers: { above: [trendLine()] },
    plugins: [
      tooltips({
        roles: ['point', 'trendLine'],
        format: item => {
          if (item.role !== 'trendLine') {
            // Points keep the default text.
            return undefined;
          }
          const { slope, correlation } = item.context;
          const fit =
            correlation === undefined
              ? ''
              : `, R² ${(correlation ** 2).toFixed(2)}`;
          return `${item.seriesName}: ${slope >= 0 ? '+' : ''}${slope.toFixed(1)} a day${fit}`;
        }
      })
    ]
  }
);
```

Thin lines are hard to hover: give them a wider stroke in your CSS, or use the keyboard, which reaches every role in `roles`.

### Roles from your own layers

The plugin doesn't keep a list of roles it knows: it works from the chart's scene. So the roles of [your own layers](https://chartist.dev/docs/custom-layers/) and of layers from other packages get tooltips the same way, once you list them. This chart's dots come from the lollipop layer of the custom layers guide, with a role of its own:

**Tooltips on a custom layer** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import '@chartist-js/tooltips/styles.css';
import './style.css';
import {
  autoScaleAxis,
  axisGrid,
  axisLabels,
  cartesianAxes,
  chart,
  stepAxis
} from 'chartist';
import { tooltips } from '@chartist-js/tooltips';
import { lollipop } from './lollipop';

// The lollipop layer gives its dots a role of its own. Listed in `roles`,
// they get tooltips. Their context follows Chartist's conventions
// (seriesIndex, valueIndex, value), so the default text knows what to show.
chart('#chart', {
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    series: [{ name: 'Visits', data: [12, 9, 14, 7, 11] }]
  },
  axes: cartesianAxes({
    x: { type: stepAxis() },
    y: { type: autoScaleAxis() }
  }),
  layers: [axisGrid(), lollipop(), axisLabels()],
  plugins: [tooltips({ roles: ['lollipop'] })]
});
```

`src/lollipop.ts`:

```ts
import { alphaNumerate, node, type Layer } from 'chartist';

// The roles this layer gives its nodes, so listeners and plugins can find
// them with a typed context.
declare module 'chartist' {
  interface NodeRoles {
    lollipop: {
      ctx: {
        readonly seriesIndex: number;
        readonly valueIndex: number;
        readonly value: number;
      };
      attrs: { readonly cx: number; readonly cy: number; readonly r: number };
    };
  }
}

export interface LollipopOptions {
  /** Series to draw, by name or index; default: all. */
  readonly series?: readonly (number | string)[];
  /** Axis ids; default: the first X and the first Y axis. */
  readonly x?: string;
  readonly y?: string;
}

/** A stem from zero and a dot for each value. */
export function lollipop(options: LollipopOptions = {}): Layer {
  return {
    type: 'lollipop',
    // Drawn where bars are, so the chart gets the bar chart's class, which
    // centers the X labels.
    chartType: 'bar',
    coordinates: ['cartesian'],
    axes: { x: options.x, y: options.y },
    series: options.series,
    // The stems start at zero, so the Y axis must include it.
    extent: ({ axes }) => (axes.y ? { [axes.y]: { low: 0, high: 0 } } : {}),
    compose({ area, axes, normalized, seriesIndexes }) {
      const { x, y } = axes;
      if (!x || !y || !normalized) {
        throw new Error('Layer "lollipop" needs an x and a y axis');
      }
      // Centered in each step, where bars sit.
      const shift = x.kind === 'step' ? (x.step ?? 0) / 2 : 0;
      const base = area.y1 - y.project(0);

      return normalized.series.map((values, local) => {
        // The series' index in the whole chart, for its class and color.
        const seriesIndex = seriesIndexes?.[local] ?? local;
        let group = node(
          'g',
          {},
          `ct-series ct-series-${alphaNumerate(seriesIndex)}`
        );
        values.forEach((value, valueIndex) => {
          // Values are normalized to { x, y }, or undefined for a hole.
          const v =
            value && typeof value === 'object' && 'y' in value
              ? value.y
              : undefined;
          if (v === undefined) {
            return;
          }
          const cx = area.x1 + shift + x.project(value, valueIndex, values);
          const cy = area.y1 - y.project(value, valueIndex, values);
          group = group
            .append(
              node('line', { x1: cx, y1: base, x2: cx, y2: cy }, 'ct-stem')
            )
            .append(
              node('circle', { cx, cy, r: 6 }, 'ct-lollipop', {
                role: 'lollipop',
                ctx: { seriesIndex, valueIndex, value: v }
              })
            );
        });
        return group;
      });
    }
  };
}
```

`src/style.css`:

```css
/* The series group sets --ct-color; stems and dots take it. */
.ct-stem {
  stroke: var(--ct-color);
  stroke-width: 2px;
}

.ct-lollipop {
  fill: var(--ct-color);
}
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

What the default text shows depends on the node's context: a layer that gives its nodes `seriesIndex`, `valueIndex` and `value`, as Chartist's own layers do, gets the label, the series' name and the value without any setup. Anything else a layer puts into a node's context reaches `format` and `render` as `item.context`. It's typed by the roles you list: checking `item.role` narrows `item.context` to that role's context, for Chartist's roles and for the roles other layers declare in `NodeRoles` alike.

```ts
import { lineChart } from 'chartist';
import { tooltips } from '@chartist-js/tooltips';

// A role of a forecast layer from another package, as such layers declare it.
declare module 'chartist' {
  interface NodeRoles {
    forecast: {
      ctx: { readonly low: number; readonly high: number };
      attrs: object;
    };
  }
}

lineChart(
  '#chart',
  { series: [[12, 15, 14]] },
  {
    plugins: [
      tooltips({
        roles: ['point', 'trendLine', 'forecast'],
        format: item => {
          if (item.role === 'forecast') {
            // Numbers here: TypeScript knows the forecast's context.
            return `Forecast: ${item.context.low} to ${item.context.high}`;
          }
          if (item.role === 'trendLine') {
            return `Trend: ${item.context.slope.toFixed(1)} a day`;
          }
          return undefined;
        }
      })
    ]
  }
);
```

Returning `undefined` keeps the default text, so a `format` like this one handles its own roles and leaves points as they were.

`position: 'pointer'` makes the tooltip follow the mouse while it moves over a value. Touch and keyboard always place it at the value: at a point, at a bar's end, at the middle of a slice's arc. It shows above, below when there's no room above, and moves sideways to stay in the viewport.

## Live charts

With [`liveDomRenderer()`](https://chartist.dev/docs/live-updates/), a tooltip stays on its value while the chart updates, shows the new value, and moves in step with the update animation. Hover a point:

**A tooltip on a live chart** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import '@chartist-js/tooltips/styles.css';
import { lineChart } from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { tooltips } from '@chartist-js/tooltips';

let visitors = [12, 15, 14, 18, 21, 19, 23, 22];

// Hover a point: the tooltip stays on it while the chart updates, and
// moves with the update animation.
const chart = lineChart(
  '#chart',
  { series: [{ name: 'Visitors', data: visitors }] },
  {
    low: 0,
    renderer: liveDomRenderer({ animations: smilAnimations() }),
    plugins: [tooltips()]
  }
);

setInterval(() => {
  const last = visitors[visitors.length - 1];
  const next = Math.max(0, last + Math.round(Math.random() * 8 - 4));
  visitors = [...visitors.slice(1), next];
  chart.update({ data: { series: [{ name: 'Visitors', data: visitors }] } });
}, 1500);
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

## Styling

The tooltip is `.ct-tooltip`, with `.ct-tooltip-title`, `.ct-tooltip-series` and `.ct-tooltip-value` inside, or `.ct-tooltip-text` for `format`'s text. Its looks are custom properties. Set them on the page, or on a chart's container for one chart:

**A styled tooltip** (runs in the browser):

`src/style.css`:

```css
/* The tooltip's look is custom properties: here on the chart's container. */
#chart {
  --ct-tooltip-background: #1e3a8a;
  --ct-tooltip-color: #fff;
  --ct-tooltip-radius: 0;
  --ct-tooltip-padding: 0.5rem 0.75rem;
  --ct-tooltip-offset: 14px;
  --ct-tooltip-hide-delay: 600ms;
}

/* And plain CSS, through the class the plugin adds. */
.fruit-tooltip .ct-tooltip-value {
  font-size: 1.25em;
  font-weight: 700;
}

/*
 * A callout arrow toward the value. The plugin says on which side the
 * tooltip is (data-placement) and where the value is from its left edge
 * (--ct-tooltip-anchor-x), even when the tooltip moved to stay in view.
 */
.fruit-tooltip::after {
  content: '';
  position: absolute;
  left: var(--ct-tooltip-anchor-x, 50%);
  translate: -50% 0;
  border: 7px solid transparent;
}

.fruit-tooltip[data-placement='top']::after {
  top: 100%;
  border-top-color: #1e3a8a;
}

.fruit-tooltip[data-placement='bottom']::after {
  bottom: 100%;
  border-bottom-color: #1e3a8a;
}
```

`src/main.ts`:

```ts
import 'chartist/styles.css';
import '@chartist-js/tooltips/styles.css';
import './style.css';
import { barChart } from 'chartist';
import { tooltips } from '@chartist-js/tooltips';

barChart(
  '#chart',
  {
    labels: ['Apples', 'Kiwis', 'Pears'],
    series: [
      { name: '2025', data: [12, 9, 7] },
      { name: '2026', data: [14, 11, 6] }
    ]
  },
  { plugins: [tooltips({ className: 'fruit-tooltip' })] }
);
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

| Property                    | What it sets                                                                    |
| --------------------------- | ------------------------------------------------------------------------------- |
| `--ct-tooltip-background`   | The background; dark on light pages and light on dark ones                      |
| `--ct-tooltip-color`        | The text color                                                                  |
| `--ct-tooltip-border`       | The border, as the `border` shorthand                                           |
| `--ct-tooltip-radius`       | The corner radius                                                               |
| `--ct-tooltip-padding`      | The padding                                                                     |
| `--ct-tooltip-font`         | The font, as the `font` shorthand                                               |
| `--ct-tooltip-shadow`       | The shadow                                                                      |
| `--ct-tooltip-max-width`    | The widest the tooltip gets before its text wraps                               |
| `--ct-tooltip-offset`       | The distance from the value, in px                                              |
| `--ct-tooltip-hide-delay`   | How long it stays after the pointer leaves                                      |
| `--ct-tooltip-focus-color`  | The color of the focused value's outline                                        |
| `--ct-tooltip-focus-width`  | The width of that outline                                                       |
| `--ct-tooltip-series-color` | Set by the plugin: the series' color, for the swatch before its name            |
| `--ct-tooltip-anchor-x`     | Set by the plugin: the value's place from the tooltip's left edge, for an arrow |

The example also draws a callout arrow toward the value. The plugin sets `data-placement` on the tooltip (`top` or `bottom`, the side of the value it shows on) and `--ct-tooltip-anchor-x`, the value's place from the tooltip's left edge, which stays right when the tooltip moved sideways to stay in view. Set `--ct-tooltip-offset` to at least the arrow's size.

The colors follow the page's `color-scheme`, like Chartist's own. `className` adds classes to the tooltip, for plain CSS beyond the properties. The tooltip stays while the pointer is over it, and the hide delay is a CSS transition, which reduced motion turns off.
