feat: update chart to recharts v3
This updates chart.tsx to recharts v3. It is based on the work done by @noxify.
- Fixes https://github.com/shadcn-ui/ui/issues/7669
- Upgrade Guide: https://ui-git-shadcn-recharts-v3-shadcn-pro.vercel.app/docs/components/chart#upgrade-guide
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| ui | Oct 20, 2025 11:47am |
💡 Enable Vercel Agent with $100 free credit for automated AI reviews
Tested the chart component and everything seems to work.
With the current state, you will have one or more warnings in the browser console:
The width(-1) and height(-1) of chart should be greater than 0, please check the style of container, or the props width(100%) and height(100%), or add a minWidth(0) or minHeight(undefined) or use aspect(undefined) to control the height and width.
Based on https://github.com/recharts/recharts/issues/2736, you can fix it by updating the ChartContainer.
Just replace:
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
with:
<RechartsPrimitive.ResponsiveContainer initialDimension={ { width: 320, height: 200 } }>{children}</RechartsPrimitive.ResponsiveContainer>
@noxify thank-you for all your work on this, seriously great job! your answer in the thread about recharts v3 support was great too.
I think the "The width(-1) and height(-1) of chart should be greater than 0" console error will hit everyone that tries the new charts using the current PR code.
What do y'all think about a revision for ChartContainer that accepts the initialDimension and sets a default value?
For example what if the ChartContainer props were like the following interface:
interface ChartContainerProps
extends Omit<React.ComponentProps<'div'>, 'children'>,
Pick<React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>, 'initialDimension' | 'children'> {
config: ChartConfig
}
So then ChartContainer would be like:
function ChartContainer({
id,
config,
initialDimension = { width: 320, height: 200 },
className,
children,
...props
}: ChartContainerProps) {
// ...
return (
// ...
<RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>
{children}
</RechartsPrimitive.ResponsiveContainer>
// ...
)
}
Come to think of it, there are perhaps a few other props that might be worth exposing from the RechartsPrimitive.ResponsiveContainer on the shadcn/ui wrapper ChartContainer...
I haven't done a dive into recharts v3 yet but a lot of them look useful e.g., setting max dimensions, passing onResize handler, etc. The recharts container can also be passed a ref and it may be useful to also support passing a ref from ChartContainer to the underlying RechartsPrimitive.ResponsiveContainer.
EDIT: an expanded interface (minus supporting a ref passthrough) could be like:
interface ChartContainerProps
extends Omit<React.ComponentProps<'div'>, 'children'>,
Pick<
React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>,
| 'initialDimension'
| 'aspect'
| 'debounce'
| 'minHeight'
| 'minWidth'
| 'maxHeight'
| 'height'
| 'width'
| 'onResize'
| 'children'
> {
config: ChartConfig
innerResponsiveContainerStyle?: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['style']
}
hey @firxworx ,
thanks for the kind words.
Providing additional props to the ChartContainer makes sense for me and will make them more usable without changing something inside the component code.
I personally like your example with the expanded interface. Seems it provides the "most needed" props from recharts.
@noxify Rebase pls 🙏
@madflow - i have no permissions here to rebase 😅
For anyone stumbling upon this while this PR is still active and wants to try out the chart.tsx for Recharts v3+, here's the complete file as drop-in replacement, credits to @noxify and @firxworx for their hard work.
"use client";
/* eslint-disable */
import {cn} from "@/lib/utils";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import type {NameType, ValueType} from "recharts/types/component/DefaultTooltipContent";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = {light: "", dark: ".dark"} as const;
export type ChartConfig = Record<
string,
{
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({color?: string; theme?: never} | {color?: never; theme: Record<keyof typeof THEMES, string>})
>;
interface ChartContextProps {
config: ChartConfig;
}
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
interface ChartContainerProps
extends
Omit<React.ComponentProps<"div">, "children">,
Pick<
React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>,
"initialDimension" | "aspect" | "debounce" | "minHeight" | "minWidth" | "maxHeight" | "height" | "width" | "onResize" | "children"
> {
config: ChartConfig;
innerResponsiveContainerStyle?: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["style"];
}
function ChartContainer({
id,
config,
initialDimension = {width: 320, height: 200},
className,
children,
...props
}: Readonly<ChartContainerProps>) {
const uniqueId = React.useId();
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{config}}>
<div
data-slot='chart'
data-chart={chartId}
className={cn(
"[&_.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-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}>
<ChartStyle
id={chartId}
config={config}
/>
<RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({id, config}: {id: string; config: ChartConfig}) => {
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"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip>
& React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
} & Omit<RechartsPrimitive.DefaultTooltipContentProps<ValueType, NameType>, "accessibilityLayer">) {
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 = getPayloadConfigFromPayload(config, item, key);
const value = !labelKey && typeof label === "string" ? (config[label]?.label ?? label) : itemConfig?.label;
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(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}>
{!nestLabel ? tooltipLabel : null}
<div className='grid gap-1.5'>
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color ?? item.payload?.fill ?? item.color;
return (
<div
key={index}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}>
{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-(--color-border) bg-(--color-bg)", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
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 != null && (
<span className='text-foreground font-mono font-medium tabular-nums'>
{typeof item.value === "number" ? item.value.toLocaleString() : String(item.value)}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
nameKey,
payload,
verticalAlign,
}: React.ComponentProps<"div"> & {
hideIcon?: boolean;
nameKey?: string;
} & RechartsPrimitive.DefaultLegendContentProps) {
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
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey ?? item.dataKey ?? "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3")}>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className='h-2 w-2 shrink-0 rounded-[2px]'
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
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];
}
export {ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent};