# Recharts Interactive Legend

> Interactive toggleable series legend to show or hide individual series dynamically.

A shadcn/ui block from the [@paceui](https://paceui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/paceui/tanstack-charts-recharts-interactive-legend).

- Page, with a live preview: https://shadcn.exchange/r/paceui/tanstack-charts-recharts-interactive-legend
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/paceui/tanstack-charts-recharts-interactive-legend-light-1431416a6b7439d8.png
- Registry item: https://paceui.com/r/tanstack-charts-recharts-interactive-legend.json
- Listed: 2026-10-21, source last changed 2026-10-21

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/paceui/tanstack-charts-recharts-interactive-legend.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tanstack/charts`, `@tanstack/react-charts`, `d3-scale`
- Registry items: `card`

## Files

### `components/tanstack-charts/line/recharts-interactive-legend.tsx`

```tsx
import { type ChartPoint, defineChart, lineY } from "@tanstack/charts";
import { controlledSignal } from "@tanstack/charts/interaction/signal";
import { interactiveColorLegend } from "@tanstack/charts/legend";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo, useState } from "react";

import { scaleLinear, scaleUtc } from "d3-scale";
import { Eye } from "lucide-react";

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";

interface IndustryDatum {
    industry: string;
    date: Date;
    unemployed: number;
}

type SeriesId = "Manufacturing" | "Construction";

const motionRenderer = motion({
    initial: "always",
    transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});

const seriesColors: Record<SeriesId, string> = {
    Manufacturing: "var(--chart-1)",
    Construction: "var(--chart-2)",
};

function generateDemoData(): IndustryDatum[] {
    const results: IndustryDatum[] = [];

    for (let m = 0; m < 14; m++) {
        const date = new Date(Date.UTC(2024, m, 1));
        results.push({
            industry: "Manufacturing",
            date,
            unemployed: Math.round(450 + Math.sin(m * 0.4) * 120 + Math.random() * 30),
        });
        results.push({
            industry: "Construction",
            date,
            unemployed: Math.round(300 + Math.cos(m * 0.4) * 90 + Math.random() * 25),
        });
    }

    return results;
}

function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
    if (points.length === 0) return { title: "", rows: [] };
    const d = points[0].datum as IndustryDatum;
    if (!d) return { title: "", rows: [] };
    return {
        title: `${d.industry} (${d.date.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: "UTC" })})`,
        rows: [
            {
                label: "Unemployed Count",
                value: `${d.unemployed}k`,
                color: seriesColors[d.industry as SeriesId] || "var(--chart-1)",
            },
        ],
    };
}

export function RechartsInteractiveLegend() {
    const [visibleSeries, setVisibleSeries] = useState<readonly SeriesId[]>(["Manufacturing", "Construction"]);

    const rawData = useMemo(() => generateDemoData(), []);

    const filteredData = useMemo(() => {
        return rawData.filter((d) => visibleSeries.includes(d.industry as SeriesId));
    }, [rawData, visibleSeries]);

    const definition = useMemo(
        () =>
            defineChart(
                {
                    renderer: motionRenderer,
                    marks: [
                        lineY(filteredData, {
                            id: "industry-lines",
                            x: "date",
                            y: "unemployed",
                            color: "industry",
                            strokeWidth: 2.5,
                        }),
                    ],
                    scales: {
                        x: {
                            scale: scaleUtc,
                            axis: {
                                ticks: {
                                    format: (v: Date) => v.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }),
                                },
                            },
                        },
                        y: {
                            scale: scaleLinear().domain([0, 750]),
                            grid: true,
                            axis: { label: "Unemployed Count (k)" },
                        },
                    },
                    color: {
                        domain: ["Manufacturing", "Construction"],
                        range: ["var(--chart-1)", "var(--chart-2)"],
                        legend: interactiveColorLegend({
                            visible: controlledSignal(visibleSeries as readonly string[], (v) =>
                                setVisibleSeries(v as readonly SeriesId[]),
                            ),
                            placement: "bottom",
                            ariaLabel: "Series visibility toggle",
                        }),
                    },
                    margin: { top: 20, right: 24, bottom: 60, left: 48 },
                    theme: {
                        foreground: "var(--muted-foreground)",
                        grid: "var(--border)",
                        background: "transparent",
                    },
                },
                {
                    focus: "group-x",
                    keyboard: true,
                    tooltip: {
                        use: tooltip,
                        className:
                            "font-sans shadow-none rounded-md bg-popover text-popover-foreground border p-2.5 text-xs pointer-events-none transition-all duration-75",
                        anchor: "pointer",
                        placement: "auto",
                        content: (points) => shadcnTooltipContent(points),
                    },
                },
            ),
        [filteredData, visibleSeries],
    );

    const toggleSeries = (id: SeriesId) => {
        if (visibleSeries.includes(id)) {
            if (visibleSeries.length > 1) {
                setVisibleSeries(visibleSeries.filter((s) => s !== id));
            }
        } else {
            setVisibleSeries([...visibleSeries, id]);
        }
    };

    return (
        <Card className="w-full max-w-xl">
            <CardHeader>
                <CardTitle>Interactive Toggleable Legend</CardTitle>
                <CardDescription>
                    Click interactive legend items to toggle series visibility on/off dynamically
                </CardDescription>
            </CardHeader>
            <CardContent className="space-y-4">
                <div className="flex items-center justify-center gap-3 pt-1">
                    {(["Manufacturing", "Construction"] as const).map((id) => {
                        const isVisible = visibleSeries.includes(id);
                        const color = seriesColors[id];
                        return (
                            <button
                                key={id}
                                type="button"
                                onClick={() => toggleSeries(id)}
                                className={`inline-flex cursor-pointer items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium transition-all duration-150 ${
                                    isVisible
                                        ? "border-border bg-muted/60 text-foreground hover:bg-muted shadow-2xs"
                                        : "text-muted-foreground border-transparent bg-transparent opacity-40 hover:opacity-70"
                                }`}>
                                <span
                                    className="h-2.5 w-2.5 rounded-full transition-transform"
                                    style={{
                                        backgroundColor: color,
                                        transform: isVisible ? "scale(1)" : "scale(0.75)",
                                    }}
                                />
                                {id}
                            </button>
                        );
                    })}
                </div>

                <div className="h-[250px] w-full">
                    <Chart
                        definition={definition}
                        renderer={motionRenderer}
                        height={250}
                        ariaLabel="Interactive Toggleable Legend Chart"
                    />
                </div>
            </CardContent>
            <CardFooter className="flex-col items-start gap-2 text-sm">
                <div className="flex items-center gap-2 leading-none font-medium">
                    Controlled signal updates visible series in state reactively{" "}
                    <Eye className="h-4 w-4 text-emerald-500" />
                </div>
                <div className="text-muted-foreground leading-none">
                    `interactiveColorLegend` integrates with React state for seamless toggle interaction
                </div>
            </CardFooter>
        </Card>
    );
}

export default RechartsInteractiveLegend;
```
