Z-Index and layers

zIndex and layers were introduced in version 3.4.

Recharts will render your components in a specific order by default. For example, Lines are on top of Areas, and Areas are on top of background.

You can see the specific defaults in DefaultZIndexes.tsx.

Components with the same z-index are rendered in the order they appended to the DOM: later components are on top of earlier ones. This is called the “painting order”, or a “painter's algorithm”.

// #region Sample data, imports, and helper functions
import {
  ComposedChart,
  Line,
  Area,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  DefaultLegendContent,
  DefaultLegendContentProps,
  DefaultTooltipContent,
  TooltipContentProps,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

const data = [
  {
    name: 'Page A',
    a: [0, 0],
    b: 0,
  },
  {
    name: 'Page B',
    a: [50, 300],
    b: 106,
  },
  {
    name: 'Page C',
    a: [150, 423],
    b: 229,
  },
  {
    name: 'Page D',
    b: 312,
  },
  {
    name: 'Page E',
    a: [367, 678],
    b: 451,
  },
  {
    name: 'Page F',
    a: [305, 821],
    b: 623,
  },
];

const renderTooltipWithoutRange = ({ payload, content, ...rest }: TooltipContentProps) => {
  const newPayload = payload.filter(x => x.dataKey !== 'a');
  return <DefaultTooltipContent payload={newPayload} {...rest} />;
};

const renderLegendWithoutRange = ({ payload, ...rest }: DefaultLegendContentProps) => {
  const newPayload = payload?.filter(x => x.dataKey !== 'a');
  return <DefaultLegendContent payload={newPayload} {...rest} />;
};
// #endregion

export default function BandedChart() {
  return (
    <ComposedChart
      style={{ width: '100%', maxWidth: '700px', maxHeight: '70vh', aspectRatio: 1.618 }}
      responsive
      data={data}
      margin={{
        top: 20,
        right: 0,
        left: 0,
        bottom: 0,
      }}
    >
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis width="auto" />
      <Legend content={renderLegendWithoutRange} />
      <Tooltip content={renderTooltipWithoutRange} />
      <Line type="natural" dataKey="b" stroke="#ff00ff" connectNulls />
      <Area type="monotone" dataKey="a" stroke="none" fill="#cccccc" connectNulls dot={false} activeDot={false} />
      <RechartsDevtools />
    </ComposedChart>
  );
}

Custom z-index

You can pass a number to the zIndex prop in various components:

Custom SVG and other Recharts components without direct zIndex support render as if they had zIndex=0. If you want to set a custom zIndex, wrap your component in ZIndexLayer.

// #region Imports, data, and helper functions
import {
  LabelList,
  Line,
  LineChart,
  LineProps,
  ReferenceDot,
  XAxis,
  YAxis,
  DefaultZIndexes,
  ZIndexLayer,
  LabelListEntry,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

const AirplaneIcon = () => (
  // https://upload.wikimedia.org/wikipedia/commons/7/7d/Plane_icon.svg
  <svg xmlns="http://www.w3.org/2000/svg" width="238.45445" height="228.24998" version="1.0">
    <path
      transform="scale(0.1)"
      fillRule="evenodd"
      d="M194.67321 0 70.641958 53.625c-10.38227-6.92107-34.20058-21.27539-38.90545-23.44898-39.4400301-18.22079-36.9454001 14.73107-20.34925 24.6052 4.53917 2.70065 27.72352 17.17823 43.47345 26.37502l17.90625 133.9375 22.21875 13.15625 11.531252-120.9375 71.53125 36.6875 3.84375 39.21875 14.53125 8.625 11.09375-42.40625.125.0625 30.8125-31.53125-14.875-8-35.625 16.90625-68.28125-42.4375L217.36071 12.25 194.67321 0z"
    />
  </svg>
);

function between(a: number, b: number) {
  return Math.min(a, b) + Math.abs(a - b) / 2;
}

const topRow = 4;
const midRow = 2;
const bottomRow = 0;
const startA = 0;
const gapA = 11;
const mustek = { name: 'Můstek', yPos: between(topRow, midRow), xPos: 94 };
const florenc = { name: 'Florenc', yPos: mustek.yPos, xPos: 127 };
const muzeum = { name: 'Muzeum', yPos: between(midRow, bottomRow), xPos: between(mustek.xPos, florenc.xPos) };
const hlavak = {
  name: 'Hlavní nádraží',
  yPos: muzeum.yPos + (florenc.yPos - muzeum.yPos) / 2,
  xPos: muzeum.xPos + (florenc.xPos - muzeum.xPos) / 2,
};

const motol = { name: 'Nemocnice Motol', yPos: topRow, xPos: startA };
const petriny = { name: 'Petřiny', yPos: topRow, xPos: motol.xPos + gapA };
const veleslavin = { name: 'Nádraží Veleslavín', yPos: topRow, xPos: petriny.xPos + gapA };
const borislavka = { name: 'Bořislavka', yPos: topRow, xPos: veleslavin.xPos + gapA };
const dejvicka = { name: 'Dejvická', yPos: topRow, xPos: borislavka.xPos + gapA };
const hradcanska = { name: 'Hradčanská', yPos: topRow, xPos: dejvicka.xPos + gapA };
const malostranska = { name: 'Malostranská', yPos: topRow, xPos: hradcanska.xPos + gapA };
const staromestska = { name: 'Staroměstská', yPos: topRow, xPos: malostranska.xPos + gapA };
// mustek
// muzeum
const mirak = { name: 'Náměstí Míru', yPos: bottomRow, xPos: muzeum.xPos + 13 };
const jzp = { name: 'Jiřího z Poděbrad', yPos: bottomRow, xPos: mirak.xPos + gapA };
const flora = { name: 'Flora', yPos: bottomRow, xPos: jzp.xPos + gapA };
const zelivarna = { name: 'Želivského', yPos: bottomRow, xPos: flora.xPos + gapA };
const strasnicka = { name: 'Strašnická', yPos: bottomRow, xPos: zelivarna.xPos + gapA };
const skalka = { name: 'Skalka', yPos: bottomRow, xPos: strasnicka.xPos + gapA };
const depohostivar = { name: 'Depo Hostivař', yPos: bottomRow, xPos: skalka.xPos + gapA };
const lineA = {
  name: 'A',
  color: '#1aa300',
  stations: [
    motol,
    petriny,
    veleslavin,
    borislavka,
    dejvicka,
    hradcanska,
    malostranska,
    staromestska,
    mustek,
    muzeum,
    mirak,
    jzp,
    flora,
    zelivarna,
    strasnicka,
    skalka,
    depohostivar,
  ],
};

const gapB = 8;
const narodka = { name: 'Národní třída', yPos: midRow, xPos: mustek.xPos - 20 };
const karlak = { name: 'Karlovo náměstí', yPos: midRow, xPos: narodka.xPos - gapB };
const andel = { name: 'Anděl', yPos: midRow, xPos: karlak.xPos - gapB };
const smichac = { name: 'Smíchovské nádraží', yPos: midRow, xPos: andel.xPos - gapB };
const radlicka = { name: 'Radlická', yPos: midRow, xPos: smichac.xPos - gapB };
const jinonice = { name: 'Jinonice', yPos: midRow, xPos: radlicka.xPos - gapB };
const novebutovice = { name: 'Nové Butovice', yPos: midRow, xPos: jinonice.xPos - gapB };
const hurka = { name: 'Hůrka', yPos: midRow, xPos: novebutovice.xPos - gapB };
const luziny = { name: 'Lužiny', yPos: midRow, xPos: hurka.xPos - gapB };
const luka = { name: 'Luka', yPos: midRow, xPos: luziny.xPos - gapB };
const stodulky = { name: 'Stodůlky', yPos: midRow, xPos: luka.xPos - gapB };
const zlicin = { name: 'Zličín', yPos: midRow, xPos: stodulky.xPos - gapB };
// mustek
const namestirepubliky = {
  name: 'Náměstí Republiky',
  yPos: mustek.yPos,
  xPos: muzeum.xPos,
};
// florenc
const krizikova = { name: 'Křižíkova', yPos: midRow, xPos: florenc.xPos + 20 };
const invalidovna = { name: 'Invalidovna', yPos: midRow, xPos: krizikova.xPos + gapB };
const ceskomoravska = { name: 'Českomoravská', yPos: midRow, xPos: invalidovna.xPos + gapB };
const palmovka = { name: 'Palmovka', yPos: midRow, xPos: ceskomoravska.xPos + gapB };
const vysocanska = { name: 'Vysočanská', yPos: midRow, xPos: palmovka.xPos + gapB };
const kolbenova = { name: 'Kolbenova', yPos: midRow, xPos: vysocanska.xPos + gapB };
const hloubetin = { name: 'Hloubětín', yPos: midRow, xPos: kolbenova.xPos + gapB };
const rajskazahrada = { name: 'Rajská zahrada', yPos: midRow, xPos: hloubetin.xPos + gapB };
const cernak = { name: 'Černý Most', yPos: midRow, xPos: rajskazahrada.xPos + gapB };
const lineB = {
  name: 'B',
  color: '#fed330',
  stations: [
    zlicin,
    stodulky,
    luka,
    luziny,
    hurka,
    novebutovice,
    jinonice,
    radlicka,
    smichac,
    andel,
    karlak,
    narodka,
    mustek,
    namestirepubliky,
    florenc,
    krizikova,
    invalidovna,
    ceskomoravska,
    palmovka,
    vysocanska,
    kolbenova,
    hloubetin,
    rajskazahrada,
    cernak,
  ],
};

const gapC = 9;
const ipak = { name: 'I. P. Pavlova', yPos: bottomRow, xPos: muzeum.xPos - 18 };
const vysehrad = { name: 'Vyšehrad', yPos: bottomRow, xPos: ipak.xPos - gapC };
const povstani = { name: 'Pražského povstání', yPos: bottomRow, xPos: vysehrad.xPos - gapC };
const pankrac = { name: 'Pankrác', yPos: bottomRow, xPos: povstani.xPos - gapC };
const budejarna = { name: 'Budějovická', yPos: bottomRow, xPos: pankrac.xPos - gapC };
const kacerov = { name: 'Kačerov', yPos: bottomRow, xPos: budejarna.xPos - gapC };
const roztyly = { name: 'Roztyly', yPos: bottomRow, xPos: kacerov.xPos - gapC };
const chodov = { name: 'Chodov', yPos: bottomRow, xPos: roztyly.xPos - gapC };
const opatov = { name: 'Opatov', yPos: bottomRow, xPos: chodov.xPos - gapC };
const haje = { name: 'Háje', yPos: bottomRow, xPos: opatov.xPos - gapC };

const vltavska = { name: 'Vltavská', yPos: topRow, xPos: florenc.xPos + 13 };
const holesky = { name: 'Nádraží Holešovice', yPos: topRow, xPos: vltavska.xPos + gapC };
const kobylisy = { name: 'Kobylisy', yPos: topRow, xPos: holesky.xPos + gapC };
const ladvi = { name: 'Ládví', yPos: topRow, xPos: kobylisy.xPos + gapC };
const letnany = { name: 'Letňany', yPos: topRow, xPos: ladvi.xPos + gapC };
const strizkov = { name: 'Střížkov', yPos: topRow, xPos: letnany.xPos + gapC };
const prosek = { name: 'Prosek', yPos: topRow, xPos: strizkov.xPos + gapC };
const lineC = {
  name: 'C',
  color: '#ff242e',
  stations: [
    haje,
    opatov,
    chodov,
    roztyly,
    kacerov,
    budejarna,
    pankrac,
    povstani,
    vysehrad,
    ipak,
    muzeum,
    hlavak,
    florenc,
    vltavska,
    holesky,
    kobylisy,
    ladvi,
    letnany,
    strizkov,
    prosek,
  ],
};

// Some labels are rendering overlapped so we need to show them in custom positions
const customLabels = [mustek, muzeum, florenc, hlavak, veleslavin].map(s => s.name);

function MetroLine(props: LineProps) {
  return (
    <Line
      type="monotone"
      dataKey="yPos"
      strokeWidth={15}
      dot={{ strokeWidth: 3, stroke: '#fff', r: 10, fill: props.stroke, fillOpacity: 1 }}
      isAnimationActive={false}
      activeDot={false}
      {...props}
    >
      <LabelList
        angle={-65}
        fill={props.stroke}
        fontWeight="bold"
        textAnchor="start"
        // dataKey="name"
        position="top"
        dy={-15}
        dx={5}
        valueAccessor={(entry: LabelListEntry) => {
          const label: string | undefined = entry.payload?.name;
          if (label != null && customLabels.includes(label)) {
            return undefined;
          }
          return label;
        }}
      />
    </Line>
  );
}
// #endregion

export default function PrahaMetro() {
  return (
    <LineChart
      width={900}
      height={480}
      margin={{
        left: 30,
        bottom: 30,
        right: 60,
        top: 150,
      }}
    >
      <MetroLine id="C" data={lineC.stations} stroke={lineC.color} />
      <MetroLine id="A" data={lineA.stations} stroke={lineA.color} />
      <MetroLine id="B" data={lineB.stations} stroke={lineB.color} />
      <XAxis type="number" dataKey="xPos" domain={['dataMin', 'dataMax']} hide />
      <YAxis domain={[0, 'dataMax']} hide />
      <ReferenceDot
        x={mustek.xPos}
        y={mustek.yPos}
        label={{
          value: mustek.name,
          fill: '#000',
          angle: -65,
          textAnchor: 'start',
          fontWeight: 'bold',
          dy: -18,
          dx: 10,
        }}
        r={18}
        fill={lineA.color}
        fillOpacity={1}
        // one lower zIndex so that these dots render as a background behind the line dots
        zIndex={DefaultZIndexes.scatter - 1}
      />
      <ReferenceDot
        x={muzeum.xPos}
        y={muzeum.yPos}
        label={{
          value: muzeum.name,
          fill: '#000',
          angle: -65,
          textAnchor: 'end',
          fontWeight: 'bold',
          dy: 45,
        }}
        r={18}
        fill={lineC.color}
        fillOpacity={1}
        // one lower zIndex so that these dots render as a background behind the line dots
        zIndex={DefaultZIndexes.scatter - 1}
      />
      <ReferenceDot
        x={florenc.xPos}
        y={florenc.yPos}
        label={{
          value: florenc.name,
          fill: '#000',
          angle: -65,
          textAnchor: 'start',
          fontWeight: 'bold',
          dy: -18,
          dx: -15,
        }}
        r={18}
        fill={lineC.color}
        fillOpacity={1}
        // one lower zIndex so that these dots render as a background behind the line dots
        zIndex={DefaultZIndexes.scatter - 1}
      />
      <ReferenceDot
        x={hlavak.xPos}
        y={hlavak.yPos}
        label={{
          value: hlavak.name,
          fill: '#000',
          angle: -73,
          textAnchor: 'start',
          fontWeight: 'bold',
          dy: 45,
          dx: 15,
        }}
        r={20}
        // one higher zIndex so that this dot renders over the line dot - we want to highlight this station
        zIndex={DefaultZIndexes.scatter + 1}
        fill={lineC.color}
        fillOpacity={1}
        stroke="#fff"
        strokeWidth={3}
      />
      <ReferenceDot
        x={veleslavin.xPos}
        y={veleslavin.yPos}
        label={{
          value: veleslavin.name,
          fill: lineA.color,
          angle: -65,
          textAnchor: 'start',
          fontWeight: 'bold',
          dy: -25,
          dx: 0,
        }}
        r={20}
        // one higher zIndex so that this dot renders over the line dot - we want to highlight this station
        zIndex={DefaultZIndexes.scatter + 1}
        fill={lineA.color}
        fillOpacity={1}
        stroke="#fff"
        strokeWidth={3}
      />
      {/* Position the Airplane icon on top of the dot */}
      <ZIndexLayer zIndex={DefaultZIndexes.scatter + 2}>
        <svg xmlns="http://www.w3.org/2000/svg" x={495} y={290}>
          <AirplaneIcon />
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" x={147} y={140}>
          <AirplaneIcon />
        </svg>
      </ZIndexLayer>
      <RechartsDevtools />
    </LineChart>
  );
}

Limitations

SVG does not support z-index directly. Recharts has a custom implementation that at first looks and feels like the Real z-index but if you dig long enough you will discover that there are some limitations:

  • z-index only works within a single chart. You cannot have a component in one chart overlap a component in another chart using z-index. You can set style={{ zIndex: 123 }}on the chart container itself to have the whole chart overlap another chart. But you cannot have individual components overlap across charts.
  • The painter's algorithm follows DOM insertion order, not JSX order. This means that if you have conditional rendering in your JSX, the actual DOM insertion order may not match the visual order you expect. If you rely on specific painting order, it's safer to use explicit zIndex values.