Shadcn Chart for React and Tailwind
Visualize data with beautiful charts.
For more complete, copy-ready setups:
Browse the Charts Examples (soon).
Installation
bunx --bun shadcn@latest add https://kit.dev/r/chart.jsonpnpm dlx shadcn@latest add https://kit.dev/r/chart.jsonnpx shadcn@latest add https://kit.dev/r/chart.jsonyarn shadcn@latest add https://kit.dev/r/chart.jsonInstall the following dependencies:
bun add rechartspnpm add rechartsnpm install rechartsyarn add rechartsImport the following chart color variables into your CSS file (if they are not already in your theme)
@theme inline {
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
:root {
--chart-1: var(--color-orange-600);
--chart-2: var(--color-teal-600);
--chart-3: var(--color-cyan-900);
--chart-4: var(--color-amber-400);
--chart-5: var(--color-amber-500);
}
.dark {
--chart-1: var(--color-blue-700);
--chart-2: var(--color-emerald-500);
--chart-3: var(--color-amber-500);
--chart-4: var(--color-purple-500);
--chart-5: var(--color-rose-500);
}Copy and paste the following code into your project.
"use client";
import React from "react";
import {
Legend,
type LegendPayload,
type LegendProps,
ResponsiveContainer,
Tooltip,
type TooltipContentProps,
type TooltipPayloadEntry,
type TooltipValueType,
} from "recharts";
import { cn } from "@/lib/utils";
const THEMES = { dark: ".dark", light: "" } as const;
export type ChartConfig = Record<
string,
(
| { color?: never; theme: Record<keyof typeof THEMES, string> }
| { color?: string; theme?: never }
) & {
icon?: React.ComponentType;
label?: React.ReactNode;
}
>;
export interface ChartLegendContentProps {
className?: string;
hideIcon?: boolean;
nameKey?: string;
payload?: LegendPayload[];
verticalAlign?: LegendProps["verticalAlign"];
}
export type CustomTooltipProps = Partial<
TooltipContentProps<TooltipValueType, NameType>
> & {
className?: string;
color?: string;
formatter?: Formatter;
hideIndicator?: boolean;
hideLabel?: boolean;
indicator?: "dashed" | "dot" | "line";
labelClassName?: string;
labelFormatter?: (
label: TooltipContentProps<number, string>["label"],
payload: TooltipContentProps<number, string>["payload"]
) => React.ReactNode;
labelKey?: string;
nameKey?: string;
};
export type Formatter<
TValue extends TooltipValueType = TooltipValueType,
TName extends NameType = NameType,
> = (
value: TValue | undefined,
name: TName | undefined,
item: TooltipPayloadEntry<TValue, TName>,
index: number,
payload: readonly TooltipPayloadEntry<TValue, TName>[]
) => [React.ReactNode, TName] | React.ReactNode;
export type NameType = number | string;
export type TooltipType = "none";
interface ChartContextProps {
config: ChartConfig;
}
const ChartContext = React.createContext<ChartContextProps | null>(null);
interface ChartContainerProps extends React.ComponentProps<"div"> {
children: React.ComponentProps<typeof ResponsiveContainer>["children"];
config: ChartConfig;
}
export const ChartContainer = (props: ChartContainerProps) => {
const { children, className, config, id, ...rest } = props;
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
className={cn(
"flex w-full min-w-0 justify-center",
"aspect-video",
"text-xs",
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
"[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50",
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border",
"[&_.recharts-dot[stroke='#fff']]:stroke-transparent",
"[&_.recharts-layer]:outline-hidden",
"[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border",
"[&_.recharts-radial-bar-background-sector]:fill-muted",
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted",
className
)}
data-chart={chartId}
data-slot="chart"
{...rest}
>
<ChartStyle config={config} id={chartId} />
<ResponsiveContainer
initialDimension={{ width: 320, height: 180 }}
minWidth={0}
>
{children}
</ResponsiveContainer>
</div>
</ChartContext.Provider>
);
};
export const ChartStyle = ({
config,
id,
}: {
config: ChartConfig;
id: string;
}) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
);
};
export const ChartTooltip = Tooltip;
export const ChartTooltipContent = (props: CustomTooltipProps) => {
const {
active,
className,
color,
formatter,
hideIndicator = false,
hideLabel = false,
indicator = "dot",
label,
labelClassName,
labelFormatter,
labelKey,
nameKey,
payload,
} = props;
const { config } = _useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayload(config, item, key);
const value = (() => {
const v =
!labelKey && typeof label === "string"
? (config[label as keyof typeof config]?.label ?? label)
: itemConfig?.label;
return typeof v === "string" || typeof v === "number" ? v : undefined;
})();
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!(active && payload?.length)) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"min-w-32",
"grid items-start gap-1.5",
"px-2.5 py-1.5",
"bg-background",
"text-xs",
"rounded-lg border border-border/50 shadow-xl",
className
)}
>
{nestLabel ? null : tooltipLabel}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
className={cn(
"w-full",
"flex flex-wrap items-stretch gap-2",
"[&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
key={key}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"my-0.5": nestLabel && indicator === "dashed",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"w-1": indicator === "line",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-medium font-mono text-foreground tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
};
export const ChartLegend = (
props: LegendProps & { className?: string }
) => {
const { className, content, ...rest } = props;
const nextContent =
className && React.isValidElement<{ className?: string }>(content)
? React.cloneElement(content, {
className: cn(className, content.props.className),
})
: content;
return <Legend content={nextContent} {...rest} />;
};
export const ChartLegendContent = (props: ChartLegendContentProps) => {
const {
className,
hideIcon = false,
nameKey,
payload,
verticalAlign = "bottom",
} = props;
const { config } = _useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayload(config, item, key);
return (
<div
className={cn(
"flex items-center gap-1.5",
"[&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
key={item.value}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="size-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
};
const getPayload = (config: ChartConfig, payload: unknown, key: string) => {
if (typeof payload !== "object" || payload === null) {
return;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
};
export const _useChart = () => {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
};Update the import paths to match your project setup.
Anatomy
ChartContainer
├── ChartStyle
├── Recharts surface (BarChart, LineChart, …)
├── ChartTooltip
│ └── ChartTooltipContent
└── ChartLegend
└── ChartLegendContentComponent
These pieces follow the familiar shadcn/ui chart pattern—container, tooltips, and legend helpers around standard Recharts charts—restyled and wired to shadcn.io tokens and imports so the result feels native to this kit.
import { Bar, BarChart } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
export const MyChart = () => {
return (
<ChartContainer>
<BarChart data={data}>
<Bar dataKey="value" />
<ChartTooltip content={(props) => <ChartTooltipContent {...props} />} />
</BarChart>
</ChartContainer>
)
}Usage
Walk through a small bar chart: sample data, a shared config object, then layers for grid, axis, tooltip, and legend.
Start with data
Shape the rows however you like; dataKey maps columns to bars, lines, or areas. This set tracks desktop versus mobile counts per month:
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
]Add a chart config
ChartConfig is metadata: human-readable labels, optional icons, and color tokens. It stays separate from the data array so several charts can reuse the same palette and copy.
import { type ChartConfig } from "@/components/ui/chart"
const chartConfig = {
desktop: {
label: "Desktop",
color: "#2563eb",
},
mobile: {
label: "Mobile",
color: "#60a5fa",
},
} satisfies ChartConfigRender the chart
Pass config into ChartContainer. Give the container a minimum height (for example min-h-[200px]) or a fixed height so Recharts can measure the SVG responsively.
"use client"
import { Bar, BarChart } from "recharts"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Add a grid
Import CartesianGrid from Recharts and nest it inside the chart. Horizontal stripes alone often read cleaner than a full mesh:
import { Bar, BarChart, CartesianGrid } from "recharts"
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Add an axis
Use XAxis (and YAxis when you need one) for categories and ticks. Here ticks are shortened and decorative lines are hidden for a lighter look:
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
<ChartContainer config={chartConfig} className="h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Add a tooltip
shadcn.io's tooltip pair reads names, swatches, and values from chartConfig automatically:
import { ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
<ChartContainer config={chartConfig} className="h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={(props) => <ChartTooltipContent {...props} />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Add a legend
ChartLegend with ChartLegendContent mirrors the same config entries:
import { ChartLegend, ChartLegendContent } from "@/components/ui/chart"
<ChartContainer config={chartConfig} className="h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={(props) => <ChartTooltipContent {...props} />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Chart config
Treat ChartConfig as the presentation contract for your series keys: labels for tooltips and legends, optional Lucide (or other) icons, and either a single color string or a theme map for light and dark. Data arrays stay focused on numbers and categories.
Because config is independent of rows, you can fetch remote data in one shape while keeping stable color tokens in code, share one config across a dashboard, or swap palettes without touching API responses.
import { Monitor } from "lucide-react"
import { type ChartConfig } from "@/components/ui/chart"
const chartConfig = {
desktop: {
label: "Desktop",
icon: Monitor,
color: "#2563eb",
theme: {
light: "#2563eb",
dark: "#dc2626",
},
},
} satisfies ChartConfigUse color for a single value that works in both themes, or theme with light / dark keys when the swatch should diverge.
Theming
You can theme series with CSS variables (ideal alongside shadcn.io's tokens), raw color strings in any supported format, or a mix.
CSS variables
shadcn.io's default theme already defines --chart-1 through --chart-5 in globals.css; see Styling for how those slots fit the rest of the design system. Point config entries at those variables so charts track global palette tweaks:
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfigHex, HSL, or OKLCH
Inline literals work when you do not need shared tokens:
const chartConfig = {
desktop: { label: "Desktop", color: "#2563eb" },
mobile: { label: "Mobile", color: "hsl(220, 98%, 61%)" },
tablet: { label: "Tablet", color: "oklch(0.5 0.2 240)" },
laptop: { label: "Laptop", color: "var(--chart-2)" },
} satisfies ChartConfigApplying colors in markup and data
The container exposes var(--color-<key>) for each config key. Use that placeholder anywhere Recharts or Tailwind can read a color:
- Series elements:
<Bar dataKey="desktop" fill="var(--color-desktop)" /> - Data-driven fills:
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" }with matching config keys - Tailwind utilities:
<LabelList className="fill-[--color-desktop]" />
Tooltip
Pair ChartTooltip with ChartTooltipContent to get a styled surface that pulls series colors and labels from chartConfig. Pass tooltip state from Recharts by using a render function for content and spreading those props into ChartTooltipContent. Toggle the label row or the color chip with props, and switch the indicator shape between dot, line, and dashed styles.
import { ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
<ChartTooltip content={(props) => <ChartTooltipContent {...props} />} />Tooltip props
| Prop | Type |
|---|---|
labelKey | string |
nameKey | string |
indicator | "dot" | "line" | "dashed" |
hideLabel | boolean |
hideIndicator | boolean |
Custom label and name keys
When the tooltip should read from different fields than the default mapping, pass labelKey and nameKey so the header and each row title resolve correctly:
const chartData = [
{ browser: "chrome", visitors: 187, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
]
const chartConfig = {
visitors: {
label: "Total Visitors",
},
chrome: {
label: "Chrome",
color: "var(--chart-1)",
},
safari: {
label: "Safari",
color: "var(--chart-2)",
},
} satisfies ChartConfig
<ChartTooltip
content={(props) => (
<ChartTooltipContent {...props} labelKey="visitors" nameKey="browser" />
)}
/>Legend
ChartLegend delegates rendering to ChartLegendContent, which builds items from the same config metadata as the tooltip.
import { ChartLegend, ChartLegendContent } from "@/components/ui/chart"
<ChartLegend content={<ChartLegendContent />} />Custom name key
When legend entries should track a field on each datum (for example browser), set nameKey:
const chartData = [
{ browser: "chrome", visitors: 187, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
]
const chartConfig = {
chrome: {
label: "Chrome",
color: "var(--chart-1)",
},
safari: {
label: "Safari",
color: "var(--chart-2)",
},
} satisfies ChartConfig
<ChartLegend content={<ChartLegendContent nameKey="browser" />} />Accessibility
Recharts’ accessibilityLayer opt-in wires keyboard traversal and screen-reader semantics into the chart canvas. Turn it on for interactive or data-critical views; leave it off for purely decorative thumbnails if you need to avoid extra focus targets.
<BarChart accessibilityLayer data={chartData}>
{/* children */}
</BarChart><LineChart accessibilityLayer data={chartData}>
{/* children */}
</LineChart>For every prop on primitives such as Bar, Line, or XAxis, refer to the Recharts API reference.