Recharts charts with themed colors, a tooltip and legend that read labels from one config, and keyboard navigation with a visible focus ring.
"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const chartData = Array.from({ length: 90 }, (_, index) => {
const date = new Date(Date.UTC(2026, 3, 1 + index))
const wave = Math.sin(index / 6) * 60 + Math.sin(index / 2.3) * 25
return {
date: date.toISOString().slice(0, 10),
desktop: Math.round(260 + wave + index * 1.6),
mobile: Math.round(180 - wave * 0.6 + index * 1.1),
}
})
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig
const ranges = [
{ value: "90", label: "90d", name: "Last 90 days" },
{ value: "30", label: "30d", name: "Last 30 days" },
{ value: "7", label: "7d", name: "Last 7 days" },
]
const dateFormat = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
timeZone: "UTC",
})
function formatDate(value: unknown) {
return dateFormat.format(new Date(`${value}T00:00:00Z`))
}
export function ChartDemo() {
const [range, setRange] = React.useState("90")
const data = chartData.slice(-Number(range))
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Visitors</CardTitle>
<CardDescription>Desktop and mobile visits per day</CardDescription>
<CardAction>
<ToggleGroup
variant="outline"
size="sm"
aria-label="Time range"
value={[range]}
onValueChange={(value) => {
if (value.length > 0) {
setRange(value[0])
}
}}
>
{ranges.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={item.name}
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</CardAction>
</CardHeader>
<CardContent>
<ChartContainer
aria-label="Area chart of desktop and mobile visitors per day"
config={chartConfig}
className="h-64 w-full"
>
<AreaChart data={data} margin={{ left: 12, right: 12 }}>
<defs>
<linearGradient id="fill-desktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.02}
/>
</linearGradient>
<linearGradient id="fill-mobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.02}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={formatDate}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="dot"
labelFormatter={formatDate}
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fill-mobile)"
stroke="var(--color-mobile)"
strokeWidth={2}
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fill-desktop)"
stroke="var(--color-desktop)"
strokeWidth={2}
stackId="a"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}pnpm dlx shadcn@latest add https://hextaui.com/r/chart.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
- 1
Add the theme tokens to your global CSS, if you haven’t yet.
- 2
Install the dependencies.
pnpm add recharts cn - 3
Copy and paste the following code into your project.
components/ui/chart.tsx "use client" import * as React from "react" import { cn } from "cn" import * as RechartsPrimitive from "recharts" import type { DefaultLegendContentProps, TooltipContentProps, TooltipProps, TooltipValueType, } from "recharts" const THEMES = { light: "", dark: ".dark" } as const const INITIAL_DIMENSION = { width: 320, height: 200 } as const type TooltipNameType = number | string type ChartConfig = Record< string, { label?: React.ReactNode icon?: React.ComponentType } & ( | { color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> } ) > type 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 <ChartContainer>.") } return context } function toCssIdent(value: string) { return value.replace(/[^\w-]/g, "-") } function toCssValue(value: string) { return value.replace(/[;{}<>]/g, "").trim() } function ChartContainer({ id, className, children, config, initialDimension = INITIAL_DIMENSION, ...props }: React.ComponentProps<"div"> & { config: ChartConfig children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer >["children"] initialDimension?: { width: number height: number } }) { const uniqueId = React.useId() const chartId = `chart-${toCssIdent(id ?? uniqueId)}` const context = React.useMemo(() => ({ config }), [config]) return ( <ChartContext.Provider value={context}> <div role="figure" data-slot="chart" data-chart={chartId} className={cn( "flex aspect-video min-w-0 justify-center text-xs tabular-nums forced-color-adjust-none [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-angle-axis-tick_text]:fill-muted-foreground [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-foreground/5 [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:rounded-md [&_.recharts-surface]:outline-hidden [&_.recharts-surface]:transition-shadow [&_.recharts-surface]:duration-150 [&_.recharts-surface]:[direction:ltr] [&_.recharts-surface_*]:outline-hidden [&_.recharts-surface:focus-visible]:ring-3 [&_.recharts-surface:focus-visible]:ring-focus-ring [&_.recharts-tooltip-wrapper[data-chart-snap]]:transition-none! [&_.recharts-wrapper]:touch-pan-y", className )} {...props} > <ChartStyle id={chartId} config={config} /> <RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension} > {children} </RechartsPrimitive.ResponsiveContainer> </div> </ChartContext.Provider> ) } function ChartStyle({ id, config }: { id: string; config: ChartConfig }) { const colorConfig = Object.entries(config).filter( ([, itemConfig]) => itemConfig.theme ?? itemConfig.color ) if (!colorConfig.length) { return null } const css = Object.entries(THEMES) .map(([theme, prefix]) => { const declarations = colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? itemConfig.color const value = color ? toCssValue(color) : "" return value ? ` --color-${toCssIdent(key)}: ${value};` : null }) .filter(Boolean) .join("\n") return `${prefix} [data-chart="${toCssIdent(id)}"] {\n${declarations}\n}` }) .join("\n") return <style dangerouslySetInnerHTML={{ __html: css }} /> } function ChartTooltip({ animationDuration = 200, animationEasing = "ease-out", ...props }: TooltipProps<TooltipValueType, TooltipNameType>) { return ( <RechartsPrimitive.Tooltip animationDuration={animationDuration} animationEasing={animationEasing} {...props} /> ) } function useNumberFormat(locale: Intl.LocalesArgument) { return React.useMemo(() => { try { return new Intl.NumberFormat(locale) } catch { return new Intl.NumberFormat("en-US") } }, [locale]) } function formatValue(value: unknown, format: Intl.NumberFormat): string { if (typeof value === "number") { return Number.isFinite(value) ? format.format(value) : String(value) } if (Array.isArray(value)) { return value.map((item) => formatValue(item, format)).join(" – ") } return value == null ? "" : String(value) } function snapTooltipWrapper(node: HTMLDivElement | null) { const wrapper = node?.parentElement if ( !wrapper?.classList.contains("recharts-tooltip-wrapper") || typeof MutationObserver === "undefined" ) { return undefined } let hidden = wrapper.style.visibility === "hidden" let from: string | null = null let frame = 0 let timeout: ReturnType<typeof setTimeout> | undefined const release = () => { from = null clearTimeout(timeout) cancelAnimationFrame(frame) frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => { wrapper.removeAttribute("data-chart-snap") }) }) } const snap = () => { cancelAnimationFrame(frame) clearTimeout(timeout) wrapper.setAttribute("data-chart-snap", "") from = wrapper.style.transform timeout = setTimeout(release, 100) } if (!hidden) { snap() } const observer = new MutationObserver(() => { const nowHidden = wrapper.style.visibility === "hidden" if (hidden && !nowHidden) { snap() } else if (from !== null && wrapper.style.transform !== from) { release() } hidden = nowHidden }) observer.observe(wrapper, { attributes: true, attributeFilter: ["style"] }) return () => { observer.disconnect() cancelAnimationFrame(frame) clearTimeout(timeout) wrapper.removeAttribute("data-chart-snap") } } type ChartTooltipPayload = TooltipContentProps< TooltipValueType, TooltipNameType >["payload"] type ChartTooltipContentProps = { active?: boolean payload?: ChartTooltipPayload label?: React.ReactNode labelFormatter?: TooltipProps< TooltipValueType, TooltipNameType >["labelFormatter"] formatter?: TooltipProps<TooltipValueType, TooltipNameType>["formatter"] valueFormatter?: ( value: TooltipValueType | undefined, name: TooltipNameType | undefined, item: ChartTooltipPayload[number] ) => React.ReactNode className?: string labelClassName?: string color?: string indicator?: "line" | "dot" | "dashed" hideLabel?: boolean hideIndicator?: boolean nameKey?: string labelKey?: string locale?: Intl.LocalesArgument } function ChartTooltipContent({ active, payload, className, indicator = "dot", hideLabel = false, hideIndicator = false, label, labelFormatter, labelClassName, formatter, valueFormatter, color, nameKey, labelKey, locale = "en-US", }: ChartTooltipContentProps) { const { config } = useChart() const format = useNumberFormat(locale) if (!active || !payload?.length) { return null } const items = payload.filter((item) => item.type !== "none") const tooltipLabel = (() => { if (hideLabel) { 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" || typeof label === "number") ? ((Object.hasOwn(config, label) ? config[label].label : undefined) ?? label) : itemConfig?.label if (labelFormatter) { const formatted = labelFormatter(value, payload) return formatted == null || formatted === "" ? null : ( <div data-slot="chart-tooltip-label" className={cn("font-medium text-pretty", labelClassName)} > {formatted} </div> ) } if (value == null || value === "") { return null } return ( <div data-slot="chart-tooltip-label" className={cn("font-medium text-pretty", labelClassName)} > {value} </div> ) })() const nestLabel = items.length === 1 && indicator !== "dot" return ( <div ref={snapTooltipWrapper} data-slot="chart-tooltip" className={cn( "grid max-w-[min(18rem,calc(100vw-2rem))] min-w-32 animate-in items-start gap-1.5 rounded-lg bg-popover px-2.5 py-1.5 text-xs wrap-anywhere text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 ease-out-quint animation-duration-150 fade-in-0 motion-safe:zoom-in-95 forced-colors:border", className )} > {!nestLabel ? tooltipLabel : null} <div className="grid gap-1.5"> {items.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 const value = valueFormatter ? valueFormatter(item.value, item.name, item) : formatValue(item.value, format) return ( <div key={`${item.graphicalItemId ?? ""}-${String(item.dataKey ?? index)}-${index}`} data-slot="chart-tooltip-item" className={cn( "flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5 [&>svg]:shrink-0 [&>svg]:text-muted-foreground", indicator === "dot" && "items-center" )} > {formatter && item.value !== undefined && item.name ? ( formatter(item.value, item.name, item, index, payload) ) : ( <> {itemConfig?.icon ? ( <itemConfig.icon /> ) : ( !hideIndicator && ( <div aria-hidden data-slot="chart-tooltip-indicator" data-indicator={indicator} style={ { "--chart-indicator": indicatorColor, } as React.CSSProperties } className={cn( "shrink-0 rounded-[calc(var(--radius-sm)*0.4)] border-(--chart-indicator) bg-(--chart-indicator)", indicator === "dot" && "size-2.5", indicator === "line" && "w-1", indicator === "dashed" && "w-0 border-[1.5px] border-dashed bg-transparent", nestLabel && indicator === "dashed" && "my-0.5" )} /> ) )} <div className={cn( "flex min-w-0 flex-1 justify-between gap-4 leading-none", nestLabel ? "items-end" : "items-center" )} > <div className="grid min-w-0 gap-1.5"> {nestLabel ? tooltipLabel : null} <span className="text-muted-foreground"> {itemConfig?.label ?? item.name} </span> </div> {value != null && value !== "" && value !== false && ( <span dir="ltr" className="shrink-0 font-medium text-foreground tabular-nums" > {value} </span> )} </div> </> )} </div> ) })} </div> </div> ) } function ChartLegend({ itemSorter = null, ...props }: React.ComponentProps<typeof RechartsPrimitive.Legend>) { return <RechartsPrimitive.Legend itemSorter={itemSorter} {...props} /> } function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey, }: Pick<DefaultLegendContentProps, "payload" | "verticalAlign"> & { className?: string hideIcon?: boolean nameKey?: string }) { const { config } = useChart() const items = payload?.filter((item) => item.type !== "none") ?? [] if (!items.length) { return null } return ( <div data-slot="chart-legend" className={cn( "flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5", verticalAlign === "top" ? "pb-3" : "pt-3", className )} > {items.map((item, index) => { const key = `${nameKey ?? item.dataKey ?? "value"}` const itemConfig = getPayloadConfigFromPayload(config, item, key) return ( <div key={`${String(item.dataKey ?? item.value ?? "")}-${index}`} data-slot="chart-legend-item" className="flex min-w-0 items-center gap-1.5 [&>svg]:size-3 [&>svg]:shrink-0 [&>svg]:text-muted-foreground" > {itemConfig?.icon && !hideIcon ? ( <itemConfig.icon /> ) : ( <div aria-hidden data-slot="chart-legend-indicator" style={ { "--chart-indicator": item.color } as React.CSSProperties } className="size-2 shrink-0 rounded-[calc(var(--radius-sm)*0.4)] bg-(--chart-indicator)" /> )} <span className="min-w-0 wrap-anywhere"> {itemConfig?.label ?? item.value} </span> </div> ) })} </div> ) } 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 Object.hasOwn(config, configLabelKey) ? config[configLabelKey] : Object.hasOwn(config, key) ? config[key] : undefined } export { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, useChart, } export type { ChartConfig, ChartTooltipContentProps } - 4
Update the import paths to match your project setup.
You build charts with Recharts components and add the parts from chart only where you need them. Recharts is not wrapped, so its own docs and upgrade guides apply as they are.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"A ChartConfig maps each data key to a label, an optional icon and a color. It stays separate from the data, so one config can serve several charts.
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig<ChartContainer
aria-label="Bar chart of desktop and mobile visitors by month"
config={chartConfig}
className="min-h-52 w-full"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>Give <ChartContainer /> a height, a min-h-* or an aspect-* so the chart can measure itself on the first render. Without one it falls back to aspect-video.
ChartContainer
└── BarChart / LineChart / AreaChart / …
├── ChartTooltip
│ └── ChartTooltipContent
└── ChartLegend
└── ChartLegendContentEach config key becomes a --color-KEY variable scoped to its chart. Point it at a theme token, at any CSS color, or give it separate light and dark values.
const chartConfig = {
desktop: {
label: "Desktop",
icon: IconDeviceDesktop,
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
theme: {
light: "oklch(0.5 0.17 285)",
dark: "oklch(0.75 0.14 190)",
},
},
} satisfies ChartConfigUse the variable wherever Recharts takes a color: on components, in your data, or in Tailwind classes.
<Bar dataKey="desktop" fill="var(--color-desktop)" />
const chartData = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
]
<LabelList className="fill-(--color-desktop)" />The theme ships five chart tokens, --chart-1 to --chart-5: blue, teal, amber, violet and rose. In this order, neighbouring colors stay distinct with the common forms of color blindness, and each one has at least 3:1 contrast against the light and dark backgrounds. Use them in order and keep the legend or labels on, so color is never the only cue.
Keys that are not valid in a CSS name, such as Mobile users, become --color-Mobile-users.
Bar chart#
A grid, an axis, <ChartTooltip /> and <ChartLegend />. The tooltip and legend read their labels and colors from the config.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
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 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig
export function ChartBar() {
return (
<ChartContainer
aria-label="Bar chart of desktop and mobile visitors by month"
config={chartConfig}
className="min-h-52 w-full"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
)
}Line chart#
indicator="line" matches the tooltip keys to the lines they describe.
"use client"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ week: "W1", signups: 412, activated: 238 },
{ week: "W2", signups: 468, activated: 301 },
{ week: "W3", signups: 395, activated: 254 },
{ week: "W4", signups: 530, activated: 362 },
{ week: "W5", signups: 588, activated: 417 },
{ week: "W6", signups: 561, activated: 431 },
{ week: "W7", signups: 642, activated: 489 },
]
const chartConfig = {
signups: {
label: "Sign-ups",
color: "var(--chart-1)",
},
activated: {
label: "Activated",
color: "var(--chart-4)",
},
} satisfies ChartConfig
export function ChartLine() {
return (
<ChartContainer
aria-label="Line chart of sign-ups and activated accounts"
config={chartConfig}
className="h-60 w-full"
>
<LineChart data={chartData} margin={{ left: 4, right: 12, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="week"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} width={36} />
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />
<Line
dataKey="signups"
type="monotone"
stroke="var(--color-signups)"
strokeWidth={2}
dot={false}
activeDot={{ r: 4 }}
/>
<Line
dataKey="activated"
type="monotone"
stroke="var(--color-activated)"
strokeWidth={2}
dot={false}
activeDot={{ r: 4 }}
/>
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
)
}Stacked bars#
Bars with the same stackId stack. Round only the outer ends, so the stack reads as one bar.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ quarter: "Q1", starter: 120, pro: 86, team: 34 },
{ quarter: "Q2", starter: 138, pro: 104, team: 51 },
{ quarter: "Q3", starter: 131, pro: 122, team: 69 },
{ quarter: "Q4", starter: 149, pro: 141, team: 88 },
]
const chartConfig = {
starter: {
label: "Starter",
color: "var(--chart-1)",
},
pro: {
label: "Pro",
color: "var(--chart-2)",
},
team: {
label: "Team",
color: "var(--chart-3)",
},
} satisfies ChartConfig
export function ChartStacked() {
return (
<ChartContainer
aria-label="Stacked bar chart of Starter, Pro and Team plans"
config={chartConfig}
className="h-60 w-full max-w-md"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="quarter"
tickLine={false}
axisLine={false}
tickMargin={10}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar
dataKey="starter"
stackId="plans"
fill="var(--color-starter)"
radius={[0, 0, 4, 4]}
/>
<Bar dataKey="pro" stackId="plans" fill="var(--color-pro)" />
<Bar
dataKey="team"
stackId="plans"
fill="var(--color-team)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
)
}Horizontal bars#
Set layout="vertical" on the chart and swap the axis types. <LabelList /> prints each value at the end of its bar.
"use client"
import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ page: "Pricing", views: 4821 },
{ page: "Docs", views: 3902 },
{ page: "Changelog", views: 2214 },
{ page: "Blog", views: 1736 },
{ page: "Careers", views: 640 },
]
const chartConfig = {
views: {
label: "Views",
color: "var(--chart-1)",
},
} satisfies ChartConfig
export function ChartHorizontal() {
return (
<ChartContainer
aria-label="Horizontal bar chart of page views"
config={chartConfig}
className="h-56 w-full max-w-md"
>
<BarChart data={chartData} layout="vertical" margin={{ right: 48 }}>
<XAxis type="number" dataKey="views" hide />
<YAxis
type="category"
dataKey="page"
tickLine={false}
axisLine={false}
tickMargin={8}
width={76}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Bar dataKey="views" fill="var(--color-views)" radius={4} barSize={20}>
<LabelList
dataKey="views"
position="right"
offset={8}
className="fill-foreground"
formatter={(value) => Number(value).toLocaleString("en-US")}
/>
</Bar>
</BarChart>
</ChartContainer>
)
}Donut#
Each slice takes its color from fill in the data. nameKey on the tooltip and legend looks up the label for each slice in the config.
"use client"
import { Label, Pie, PieChart } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
{ browser: "other", visitors: 90, fill: "var(--color-other)" },
]
const chartConfig = {
visitors: {
label: "Visitors",
},
chrome: {
label: "Chrome",
color: "var(--chart-1)",
},
safari: {
label: "Safari",
color: "var(--chart-2)",
},
firefox: {
label: "Firefox",
color: "var(--chart-3)",
},
edge: {
label: "Edge",
color: "var(--chart-4)",
},
other: {
label: "Other",
color: "var(--chart-5)",
},
} satisfies ChartConfig
const total = chartData.reduce((sum, item) => sum + item.visitors, 0)
export function ChartPie() {
return (
<ChartContainer
aria-label="Pie chart of visitors by browser"
config={chartConfig}
className="aspect-square h-72"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent nameKey="browser" hideLabel />}
/>
<Pie
data={chartData}
dataKey="visitors"
nameKey="browser"
innerRadius={58}
paddingAngle={2}
cornerRadius={4}
>
<Label
content={({ viewBox }) => {
if (!viewBox || !("cx" in viewBox)) {
return null
}
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-semibold"
>
{total.toLocaleString("en-US")}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy ?? 0) + 22}
className="fill-muted-foreground"
>
Visitors
</tspan>
</text>
)
}}
/>
</Pie>
<ChartLegend content={<ChartLegendContent nameKey="browser" />} />
</PieChart>
</ChartContainer>
)
}Radar#
Polar grids and axes pick up the same muted strokes and labels as cartesian ones.
"use client"
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ skill: "Speed", current: 82, target: 90 },
{ skill: "Quality", current: 74, target: 85 },
{ skill: "Reach", current: 61, target: 70 },
{ skill: "Retention", current: 88, target: 85 },
{ skill: "Support", current: 69, target: 80 },
{ skill: "Pricing", current: 77, target: 75 },
]
const chartConfig = {
current: {
label: "Current",
color: "var(--chart-1)",
},
target: {
label: "Target",
color: "var(--chart-3)",
},
} satisfies ChartConfig
export function ChartRadar() {
return (
<ChartContainer
aria-label="Radar chart of current and target scores"
config={chartConfig}
className="aspect-square h-80"
>
<RadarChart data={chartData}>
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />
<PolarGrid />
<PolarAngleAxis dataKey="skill" />
<Radar
dataKey="current"
stroke="var(--color-current)"
strokeWidth={2}
fill="var(--color-current)"
fillOpacity={0.15}
/>
<Radar
dataKey="target"
stroke="var(--color-target)"
strokeWidth={2}
strokeDasharray="4 4"
fill="none"
/>
<ChartLegend content={<ChartLegendContent />} />
</RadarChart>
</ChartContainer>
)
}Tooltip#
Switch between dot, line and dashed indicators, and hide the label with hideLabel. defaultIndex on <ChartTooltip /> shows a point on the first render.
"use client"
import * as React from "react"
import { Bar, BarChart, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartTooltipContentProps,
type ChartConfig,
} from "@/components/ui/chart"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const chartData = [
{ day: "Mon", running: 450, swimming: 300 },
{ day: "Tue", running: 380, swimming: 420 },
{ day: "Wed", running: 520, swimming: 120 },
{ day: "Thu", running: 140, swimming: 550 },
{ day: "Fri", running: 600, swimming: 350 },
{ day: "Sat", running: 480, swimming: 400 },
]
const chartConfig = {
running: {
label: "Running",
color: "var(--chart-1)",
},
swimming: {
label: "Swimming",
color: "var(--chart-2)",
},
} satisfies ChartConfig
type Indicator = NonNullable<ChartTooltipContentProps["indicator"]>
const indicators: Indicator[] = ["dot", "line", "dashed"]
export function ChartTooltipIndicators() {
const [indicator, setIndicator] = React.useState<Indicator>("dot")
const [hideLabel, setHideLabel] = React.useState(false)
return (
<div className="flex w-full max-w-md flex-col items-center gap-4">
<div className="flex flex-wrap justify-center gap-2">
<ToggleGroup
variant="outline"
size="sm"
aria-label="Indicator"
value={[indicator]}
onValueChange={(value) => {
if (value.length > 0) {
setIndicator(value[0] as Indicator)
}
}}
>
{indicators.map((item) => (
<ToggleGroupItem key={item} value={item}>
{item}
</ToggleGroupItem>
))}
</ToggleGroup>
<ToggleGroup
variant="outline"
size="sm"
aria-label="Label"
value={hideLabel ? ["hidden"] : []}
onValueChange={(value) => setHideLabel(value.length > 0)}
>
<ToggleGroupItem value="hidden">hideLabel</ToggleGroupItem>
</ToggleGroup>
</div>
<ChartContainer
aria-label="Bar chart of running and swimming time"
config={chartConfig}
className="h-52 w-full"
>
<BarChart data={chartData}>
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={10}
/>
<ChartTooltip
defaultIndex={2}
content={
<ChartTooltipContent
indicator={indicator}
hideLabel={hideLabel}
/>
}
/>
<Bar
dataKey="running"
stackId="a"
fill="var(--color-running)"
radius={[0, 0, 4, 4]}
/>
<Bar
dataKey="swimming"
stackId="a"
fill="var(--color-swimming)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
</div>
)
}Formatting values#
valueFormatter formats the number and keeps the indicator and name. labelFormatter does the same for the label. To replace a whole row instead, use formatter.
"use client"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ month: "2026-01", revenue: 18420.5 },
{ month: "2026-02", revenue: 21310 },
{ month: "2026-03", revenue: 19875.25 },
{ month: "2026-04", revenue: 24960 },
{ month: "2026-05", revenue: 28140.75 },
{ month: "2026-06", revenue: 31295 },
]
const chartConfig = {
revenue: {
label: "Revenue",
color: "var(--chart-1)",
},
} satisfies ChartConfig
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
const compactCurrency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
})
const monthFormat = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
})
function formatMonth(value: unknown) {
return monthFormat.format(new Date(`${value}-01T00:00:00Z`))
}
export function ChartValueFormat() {
return (
<ChartContainer
aria-label="Area chart of revenue"
config={chartConfig}
className="h-56 w-full max-w-md"
>
<AreaChart data={chartData} margin={{ left: 4, right: 12, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => formatMonth(value).slice(0, 3)}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={48}
tickFormatter={(value) => compactCurrency.format(value)}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
labelFormatter={formatMonth}
valueFormatter={(value) => currency.format(Number(value))}
/>
}
/>
<Area
dataKey="revenue"
type="monotone"
stroke="var(--color-revenue)"
strokeWidth={2}
fill="var(--color-revenue)"
fillOpacity={0.1}
/>
</AreaChart>
</ChartContainer>
)
}Icons#
An icon in the config replaces the color key in the tooltip and legend.
"use client"
import {
IconDeviceDesktop,
IconDeviceMobile,
IconDeviceTablet,
} from "@tabler/icons-react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ month: "Jan", desktop: 186, tablet: 42, mobile: 80 },
{ month: "Feb", desktop: 305, tablet: 61, mobile: 200 },
{ month: "Mar", desktop: 237, tablet: 54, mobile: 120 },
{ month: "Apr", desktop: 173, tablet: 70, mobile: 190 },
]
const chartConfig = {
desktop: {
label: "Desktop",
icon: IconDeviceDesktop,
color: "var(--chart-1)",
},
tablet: {
label: "Tablet",
icon: IconDeviceTablet,
color: "var(--chart-3)",
},
mobile: {
label: "Mobile",
icon: IconDeviceMobile,
color: "var(--chart-2)",
},
} satisfies ChartConfig
export function ChartIcons() {
return (
<ChartContainer
aria-label="Bar chart of desktop, tablet and mobile visitors"
config={chartConfig}
className="h-56 w-full max-w-md"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={10}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="tablet" fill="var(--color-tablet)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
)
}Light and dark colors#
A theme object picks a different color in each theme. Switch the site theme to see it change.
"use client"
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ hour: "08:00", latency: 182 },
{ hour: "10:00", latency: 164 },
{ hour: "12:00", latency: 231 },
{ hour: "14:00", latency: 205 },
{ hour: "16:00", latency: 148 },
{ hour: "18:00", latency: 171 },
]
const chartConfig = {
latency: {
label: "Latency (ms)",
theme: {
light: "oklch(0.5 0.17 285)",
dark: "oklch(0.75 0.14 190)",
},
},
} satisfies ChartConfig
export function ChartTheme() {
return (
<ChartContainer
aria-label="Line chart of latency in milliseconds"
config={chartConfig}
className="h-52 w-full max-w-md"
>
<LineChart data={chartData} margin={{ left: 12, right: 12, top: 8 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="hour"
padding={{ left: 16, right: 16 }}
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />
<Line
dataKey="latency"
type="linear"
stroke="var(--color-latency)"
strokeWidth={2}
dot={{ r: 3, fill: "var(--color-latency)" }}
/>
</LineChart>
</ChartContainer>
)
}Negative values#
Color each bar with <Cell /> and mark zero with <ReferenceLine />.
"use client"
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ReferenceLine,
XAxis,
} from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ month: "Jan", change: 12.4 },
{ month: "Feb", change: -4.1 },
{ month: "Mar", change: 8.7 },
{ month: "Apr", change: -9.3 },
{ month: "May", change: 3.2 },
{ month: "Jun", change: 15.8 },
]
const chartConfig = {
change: {
label: "Change",
},
gain: {
label: "Gain",
color: "var(--chart-1)",
},
loss: {
label: "Loss",
color: "var(--chart-5)",
},
} satisfies ChartConfig
export function ChartNegative() {
return (
<ChartContainer
aria-label="Bar chart of gains and losses"
config={chartConfig}
className="h-56 w-full max-w-md"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={10}
/>
<ReferenceLine y={0} />
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideIndicator
valueFormatter={(value) =>
`${Number(value) > 0 ? "+" : ""}${value}%`
}
/>
}
/>
<Bar dataKey="change" radius={4}>
{chartData.map((item) => (
<Cell
key={item.month}
fill={item.change < 0 ? "var(--color-loss)" : "var(--color-gain)"}
/>
))}
</Bar>
</BarChart>
</ChartContainer>
)
}Long labels#
Long series names wrap in the legend and the tooltip, and large values stay on one line beside them.
"use client"
import { Bar, BarChart, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{
region: "North America and the Caribbean",
enterprise: 1284032,
midMarket: 842311,
selfServe: 402118,
partners: 220940,
marketplace: 98110,
},
{
region: "Europe, Middle East and Africa",
enterprise: 1032544,
midMarket: 760201,
selfServe: 515720,
partners: 180332,
marketplace: 121004,
},
{
region: "Asia Pacific",
enterprise: 690210,
midMarket: 612090,
selfServe: 688402,
partners: 140220,
marketplace: 160550,
},
]
const chartConfig = {
enterprise: {
label: "Enterprise contracts (annual)",
color: "var(--chart-1)",
},
midMarket: {
label: "Mid-market subscriptions",
color: "var(--chart-2)",
},
selfServe: {
label: "Self-serve card payments",
color: "var(--chart-3)",
},
partners: {
label: "Reseller and agency partners",
color: "var(--chart-4)",
},
marketplace: {
label: "Cloud marketplace listings",
color: "var(--chart-5)",
},
} satisfies ChartConfig
export function ChartLongLabels() {
return (
<ChartContainer
aria-label="Bar chart of revenue by sales channel"
config={chartConfig}
className="h-80 w-full max-w-sm"
>
<BarChart data={chartData}>
<XAxis
dataKey="region"
tickLine={false}
axisLine={false}
tickMargin={10}
tickFormatter={(value) => value.split(/[ ,]/)[0]}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="enterprise" stackId="a" fill="var(--color-enterprise)" />
<Bar dataKey="midMarket" stackId="a" fill="var(--color-midMarket)" />
<Bar dataKey="selfServe" stackId="a" fill="var(--color-selfServe)" />
<Bar dataKey="partners" stackId="a" fill="var(--color-partners)" />
<Bar
dataKey="marketplace"
stackId="a"
fill="var(--color-marketplace)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
)
}Right to left#
Recharts does not mirror axes on its own. Set reversed on the x-axis and move the y-axis to the right. Pass locale to the tooltip to show the reader's digits.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const chartData = [
{ month: "يناير", desktop: 186, mobile: 80 },
{ month: "فبراير", desktop: 305, mobile: 200 },
{ month: "مارس", desktop: 237, mobile: 120 },
{ month: "أبريل", desktop: 73, mobile: 190 },
{ month: "مايو", desktop: 209, mobile: 130 },
{ month: "يونيو", desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: "سطح المكتب",
color: "var(--chart-1)",
},
mobile: {
label: "الجوال",
color: "var(--chart-2)",
},
} satisfies ChartConfig
const numbers = new Intl.NumberFormat("ar-EG")
export function ChartRtl() {
return (
<div dir="rtl" className="w-full max-w-md">
<ChartContainer
aria-label="مخطط أعمدة لزوار سطح المكتب والجوال"
config={chartConfig}
className="h-60 w-full"
>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
reversed
tickLine={false}
axisLine={false}
tickMargin={10}
/>
<YAxis
orientation="right"
tickLine={false}
axisLine={false}
tickMargin={8}
width={36}
tickFormatter={(value) => numbers.format(value)}
/>
<ChartTooltip content={<ChartTooltipContent locale="ar-EG" />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
</div>
)
}Recharts turns on accessibilityLayer by default, so the chart is one tab stop that you move through point by point.
| Key | Action |
|---|---|
| Tab | Focuses the chart and shows a focus ring. |
| ←→ | Moves the tooltip to the next or previous point. In a reversed axis the keys follow the screen. |
| Enter | Shows or hides the tooltip at the current point. |
- Charts are images to screen readers. Name each one with a visible title nearby or an
aria-labelon the Recharts chart, and offer a table when people need the exact values. - The tooltip and legend name every series in text, so a reader never has to match colors alone.
- With reduced motion, Recharts skips its enter animations and the tooltip jumps between points instead of gliding.
- Tooltip values are formatted in
en-USunless you passlocale, so the server and browser render the same text.
The parts sit on top of Recharts. Every Recharts prop still works on the chart components you put inside.
Provides the config, writes the color variables and renders a Recharts <ResponsiveContainer /> around its child. Takes every <div> prop.
| Prop | Type | Default |
|---|---|---|
configLabels, icons and colors for each data key. | ChartConfig | – |
childrenOne Recharts chart, such as <BarChart />. | ReactElement | – |
initialDimensionThe size used before the container is measured. | { width: number; height: number } | { width: 320, height: 200 } |
idNames the chart in data-chart. Generated when omitted. | string | – |
| Attribute | Description |
|---|---|
data-slot="chart" | The container. |
data-chart | The id the color variables are scoped to. |
--color-KEY | One color variable per configured key. |
| Prop | Type | Default |
|---|---|---|
label | ReactNode | – |
iconReplaces the color key in the tooltip and legend. | ComponentType | – |
colorAny CSS color or variable. | string | – |
themeUse instead of color for a color per theme. | { light: string; dark: string } | – |
The Recharts <Tooltip />, with a shorter glide between points. Takes every Recharts tooltip prop.
| Prop | Type | Default |
|---|---|---|
contentUsually <ChartTooltipContent />. | ReactElement | (props) => ReactNode | – |
animationDuration | number | 200 |
animationEasing | string | "ease-out" |
defaultIndexShows the tooltip at this point on first render. | number | – |
| Prop | Type | Default |
|---|---|---|
indicator | "dot" | "line" | "dashed" | "dot" |
hideLabel | boolean | false |
hideIndicator | boolean | false |
labelKeyThe config or data key to use for the label. | string | – |
nameKeyThe config or data key to use for each name. | string | – |
valueFormatterFormats each value and keeps the indicator and name. | (value, name, item) => ReactNode | – |
labelFormatter | (label, payload) => ReactNode | – |
formatterReplaces each row. | (value, name, item, index, payload) => ReactNode | – |
localeFormats numeric values. | Intl.LocalesArgument | "en-US" |
colorOne indicator color for every row. | string | – |
className | string | – |
labelClassName | string | – |
| Attribute | Description |
|---|---|
data-slot="chart-tooltip" | The box. |
data-slot="chart-tooltip-label" | The label. |
data-slot="chart-tooltip-item" | One row per series. |
data-slot="chart-tooltip-indicator" | The color key. |
data-indicator | On the key: dot, line or dashed. |
--chart-indicator | The key's color. |
The Recharts <Legend />. Takes every Recharts legend prop, and lists series in the order you declare them instead of alphabetically.
| Prop | Type | Default |
|---|---|---|
contentUsually <ChartLegendContent />. | ReactElement | (props) => ReactNode | – |
itemSorternull keeps the declared order. | "value" | "dataKey" | (item) => number | string | null | null |
| Prop | Type | Default |
|---|---|---|
hideIcon | boolean | false |
nameKeyThe config or data key to use for each name. | string | – |
className | string | – |
| Attribute | Description |
|---|---|
data-slot="chart-legend" | The list. |
data-slot="chart-legend-item" | One entry per series. |
data-slot="chart-legend-indicator" | The color swatch. |
Returns { config } inside a <ChartContainer />, for building your own tooltip or legend content.