Custom Indicator

Create custom tooltip indicators for charts using the useChart hook

Preview

Overview

Custom indicators allow you to replace the default tooltip crosshair and dots with your own animated elements. This is useful for creating unique visual feedback like rising lines, custom shapes, or other interactive effects.

The demo above shows a grouped bar chart with two series—one with a gradient fill and one with a diagonal pattern—each with its own animated line indicator that rises on hover.

Disabling Default Indicators

First, disable the built-in indicators on ChartTooltip:

<ChartTooltip 
  showCrosshair={false}  // Hides the vertical crosshair line
  showDots={false}       // Hides the dots on bars/lines
/>

Creating an Animated Line Indicator

Step 1: Create the Animated Element

Use motion/react with useSpring for smooth spring animations:

import { motion, useSpring } from "motion/react";
import { useEffect } from "react";

function AnimatedBarLine({
  barX,
  barTopY,
  barBottomY,
  width,
  isHovered,
}: {
  barX: number;
  barTopY: number;
  barBottomY: number;
  width: number;
  isHovered: boolean;
}) {
  // Spring animations for position and opacity
  const animatedY = useSpring(barBottomY, { stiffness: 300, damping: 30 });
  const animatedOpacity = useSpring(0, { stiffness: 300, damping: 30 });

  useEffect(() => {
    // Rise to bar top when hovered, drop to bottom when not
    animatedY.set(isHovered ? barTopY : barBottomY);
    animatedOpacity.set(isHovered ? 1 : 0);
  }, [isHovered, barTopY, barBottomY, animatedY, animatedOpacity]);

  return (
    <motion.rect
      fill="var(--chart-indicator-color)"
      height={2}
      style={{
        opacity: animatedOpacity,
        y: animatedY,
      }}
      width={width}
      x={barX}
    />
  );
}

Step 2: Access Chart State with useChart

The useChart hook provides all the data needed to position your indicator:

import { useChart } from "@bklitui/ui/charts";

function BarHorizontalLineIndicator({ data, dataKeys }) {
  const {
    barScale,        // Scale to get x position from category
    bandWidth,       // Width of each bar group
    innerHeight,     // Chart height (for bottom position)
    yScale,          // Scale to get y position from value
    hoveredBarIndex, // Which bar group is currently hovered
    margin,          // Chart margins
    containerRef,    // Ref for portal rendering
  } = useChart();
  
  // For grouped bars, divide bandWidth by number of series
  const individualBarWidth = bandWidth / dataKeys.length;
  
  // ... render indicators
}

Step 3: Render via Portal

Use a portal to render the SVG overlay in the chart container. For grouped bar charts with multiple series, calculate each bar's position within the group:

import React, { useEffect } from "react";

function BarHorizontalLineIndicator({ data, dataKeys }) {
  const { barScale, bandWidth, innerHeight, margin, containerRef, hoveredBarIndex, yScale } = useChart();
  const [mounted, setMounted] = React.useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const container = containerRef.current;
  if (!(mounted && container && bandWidth && barScale)) {
    return null;
  }

  const { createPortal } = require("react-dom");

  // Calculate individual bar width for grouped bars
  const barCount = dataKeys.length;
  const individualBarWidth = bandWidth / barCount;

  return createPortal(
    <svg
      aria-hidden="true"
      className="pointer-events-none absolute inset-0 z-50"
      height="100%"
      width="100%"
    >
      <g transform={`translate(${margin.left},${margin.top})`}>
        {data.map((d, i) => {
          const groupX = barScale(d.month) ?? 0;
          const isHovered = hoveredBarIndex === i;

          return dataKeys.map((dataKey, barIndex) => {
            const barTopY = yScale(d[dataKey]) ?? innerHeight;
            const barX = groupX + barIndex * individualBarWidth;

            return (
              <AnimatedBarLine
                key={`${d.month}-${dataKey}`}
                barX={barX}
                barTopY={barTopY}
                barBottomY={innerHeight}
                width={individualBarWidth}
                isHovered={isHovered}
              />
            );
          });
        })}
      </g>
    </svg>,
    container
  );
}

Step 4: Add to Your Chart

Add the custom indicator as a child of your chart component. You can use gradients and patterns for different series:

import { PatternLines } from "@bklitui/ui/charts";

<BarChart data={data} xDataKey="month" barGap={0}>
  <LinearGradient id="gradient" from="var(--chart-3)" to="transparent" />
  <PatternLines
    id="diagonalPattern"
    height={6}
    width={6}
    stroke="var(--chart-4)"
    strokeWidth={1.5}
    orientation={["diagonal"]}
  />
  <Grid horizontal />
  <Bar dataKey="revenue" fill="url(#gradient)" stroke="var(--chart-3)" />
  <Bar dataKey="cost" fill="url(#diagonalPattern)" stroke="var(--chart-4)" />
  <BarXAxis />
  <ChartTooltip showCrosshair={false} showDots={false} />
  <BarHorizontalLineIndicator data={data} dataKeys={["revenue", "cost"]} />
</BarChart>

Key useChart Values for Indicators

ValueTypeDescription
hoveredBarIndexnumber | nullIndex of the currently hovered bar
barScaleScaleBandBand scale for categorical x-axis positions
bandWidthnumberWidth of each bar band
yScaleScaleLinearLinear scale for y-axis values
innerHeightnumberChart area height (excluding margins)
marginMarginChart margins { top, right, bottom, left }
containerRefRefObjectRef to the chart container (for portals)
tooltipDataTooltipData | nullCurrent tooltip data including position

Theming

Use CSS variables for proper light/dark mode support:

// Use chartCssVars or CSS variables directly
<motion.rect fill="var(--chart-indicator-color)" />

Available indicator variables:

  • --chart-indicator-color - Primary indicator color
  • --chart-indicator-secondary-color - Secondary/stroke color

See Theming for the full list.