import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ 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 config = {
desktop: { label: "Desktop" , color: "var(--chart-1)" },
mobile: { label: "Mobile" , color: "var(--chart-2)" },
} satisfies ChartConfig ;
export default function ChartDemo () {
return (
< ChartContainer config = {config} className = "min-h-[200px] w-full max-w-lg" >
< BarChart data = {data}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "month"
tickLine = { false }
tickMargin = { 10 }
axisLine = { false }
tickFormatter = {( v ) => v. 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 >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/chart
The CLI installs dependencies and any other components this one uses.
Charts are built with Recharts 3, which the install adds as a dependency.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const config = {
desktop: { label: "Desktop" , color: "var(--chart-1)" },
mobile: { label: "Mobile" , color: "var(--chart-2)" },
} satisfies ChartConfig ;
< ChartContainer config = {config} className = "h-[240px] w-full" >
< BarChart data = {data}>
< 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 >
Config keys become CSS variables
Every key in config with a color or theme becomes a --color-<key>
variable scoped to that chart's container, so series use
fill="var(--color-desktop)". The same keys give the tooltip and legend
their labels and icons. The variables only exist inside the container; for
a custom legend rendered outside it, use the config's color value directly.
Chart: to show trends, comparisons or composition over many data points.
Meter or Progress Bar : a single value against a known range.
Card with a large number: when the one figure matters more than its shape. Add a small sparkline as in the KPI recipe .
Table : when users need exact values or will compare individual rows.
Pick the chart for the question: bars to compare categories, lines for trends over time, stacked areas for how parts add up over time, and pies or donuts only for a few parts of one whole.
< ChartContainer config = {config}> { /* responsive wrapper + <style> with --color-* */ }
< BarChart data = {data}> { /* any Recharts chart */ }
< CartesianGrid /> { /* restyled to the border token */ }
< XAxis /> { /* tick text uses muted-foreground */ }
< ChartTooltip { /* = Recharts Tooltip */ }
content = {< ChartTooltipContent />} { /* themed tooltip body */ }
/>
< ChartLegend { /* = Recharts Legend */ }
content = {< ChartLegendContent />} { /* themed legend body */ }
/>
< Bar dataKey = "desktop" fill = "var(--color-desktop)" />
</ BarChart >
</ ChartContainer >
Part Renders Notes ChartContainer<div> › <style> + ResponsiveContainerProvides the config through context, writes the --color-* variables for light and .dark, and sizes the chart to the div. Defaults to aspect-video. ChartTooltipRecharts Tooltip Re-exported unchanged. Pass content={<ChartTooltipContent />}. ChartTooltipContent<div>Label plus one row per series: indicator, config label, and the value formatted with toLocaleString(). ChartLegendRecharts Legend Re-exported unchanged. Pass content={<ChartLegendContent />}. ChartLegendContent<div>A centered row of swatches (or config icons) with config labels. ChartConfigtype Record<key, { label?, icon?, color? } or { label?, icon?, theme: { light, dark } }>.useCharthook Returns { config } inside a ChartContainer, for custom tooltip or legend content.
Bars compare categories. Set a radius for rounded tops, hide the axis lines and use vertical={false} on the grid for a quiet background. The hero example above adds a legend below the chart.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ 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 config = {
desktop: { label: "Desktop" , color: "var(--chart-1)" },
mobile: { label: "Mobile" , color: "var(--chart-2)" },
} satisfies ChartConfig ;
export default function ChartDemo () {
return (
< ChartContainer config = {config} className = "min-h-[200px] w-full max-w-lg" >
< BarChart data = {data}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "month"
tickLine = { false }
tickMargin = { 10 }
axisLine = { false }
tickFormatter = {( v ) => v. 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 >
);
}
Set layout="vertical" on BarChart, make the YAxis a category axis and the XAxis a hidden number axis. Horizontal bars suit long category labels like page paths or names.
import { Bar, BarChart, XAxis, YAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ page: "/pricing" , views: 4210 },
{ page: "/docs" , views: 3580 },
{ page: "/blog/launch" , views: 2240 },
{ page: "/changelog" , views: 1390 },
{ page: "/careers" , views: 820 },
];
const config = {
views: { label: "Page views" , color: "var(--chart-1)" },
} satisfies ChartConfig ;
export default function ChartBarHorizontal () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[240px] w-full max-w-lg"
>
< BarChart data = {data} layout = "vertical" margin = {{ left: 8 , right: 16 }}>
< XAxis type = "number" dataKey = "views" hide />
< YAxis
type = "category"
dataKey = "page"
tickLine = { false }
axisLine = { false }
width = { 88 }
/>
< ChartTooltip cursor = { false } content = {< ChartTooltipContent />} />
< Bar dataKey = "views" fill = "var(--color-views)" radius = { 4 } />
</ BarChart >
</ ChartContainer >
);
}
Give bars the same stackId to stack them. Round only the outer corners with a four-value radius, so the stack reads as one bar.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ week: "W36" , resolved: 42 , open: 12 },
{ week: "W37" , resolved: 51 , open: 9 },
{ week: "W38" , resolved: 38 , open: 17 },
{ week: "W39" , resolved: 64 , open: 8 },
{ week: "W40" , resolved: 57 , open: 11 },
{ week: "W41" , resolved: 70 , open: 6 },
];
const config = {
resolved: { label: "Resolved" , color: "var(--chart-1)" },
open: { label: "Still open" , color: "var(--chart-2)" },
} satisfies ChartConfig ;
export default function ChartBarStacked () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[240px] w-full max-w-lg"
>
< BarChart data = {data}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "week"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< ChartTooltip content = {< ChartTooltipContent />} />
< ChartLegend content = {< ChartLegendContent />} />
< Bar
dataKey = "resolved"
stackId = "tickets"
fill = "var(--color-resolved)"
radius = {[ 0 , 0 , 4 , 4 ]}
/>
< Bar
dataKey = "open"
stackId = "tickets"
fill = "var(--color-open)"
radius = {[ 4 , 4 , 0 , 0 ]}
/>
</ BarChart >
</ ChartContainer >
);
}
An area chart emphasizes volume over time. A linearGradient in <defs> that fades the series color gives the fill depth; reference it with fill="url(#id)".
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ month: "Jan" , visitors: 1860 },
{ month: "Feb" , visitors: 3050 },
{ month: "Mar" , visitors: 2370 },
{ month: "Apr" , visitors: 1730 },
{ month: "May" , visitors: 2090 },
{ month: "Jun" , visitors: 2140 },
];
const config = {
visitors: { label: "Visitors" , color: "var(--chart-1)" },
} satisfies ChartConfig ;
export default function ChartArea () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[220px] w-full max-w-lg"
>
< AreaChart data = {data} margin = {{ left: 12 , right: 12 }}>
< defs >
< linearGradient id = "fillVisitors" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop
offset = "5%"
stopColor = "var(--color-visitors)"
stopOpacity = { 0.8 }
/>
< stop
offset = "95%"
stopColor = "var(--color-visitors)"
stopOpacity = { 0.1 }
/>
</ linearGradient >
</ defs >
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "month"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< ChartTooltip
cursor = { false }
content = {< ChartTooltipContent indicator = "line" />}
/>
< Area
dataKey = "visitors"
type = "natural"
fill = "url(#fillVisitors)"
stroke = "var(--color-visitors)"
/>
</ AreaChart >
</ ChartContainer >
);
}
Stack areas with a shared stackId to show how parts add up to a total over time. Use a tickFormatter on the YAxis to keep large numbers short.
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ month: "Apr" , organic: 3200 , paid: 1400 , referral: 600 },
{ month: "May" , organic: 3600 , paid: 1800 , referral: 750 },
{ month: "Jun" , organic: 3400 , paid: 2400 , referral: 700 },
{ month: "Jul" , organic: 4100 , paid: 2100 , referral: 980 },
{ month: "Aug" , organic: 4700 , paid: 2600 , referral: 1100 },
{ month: "Sep" , organic: 5200 , paid: 2300 , referral: 1350 },
];
const config = {
organic: { label: "Organic" , color: "var(--chart-1)" },
paid: { label: "Paid" , color: "var(--chart-2)" },
referral: { label: "Referral" , color: "var(--chart-3)" },
} satisfies ChartConfig ;
export default function ChartAreaStacked () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[260px] w-full max-w-lg"
>
< AreaChart data = {data} margin = {{ left: 0 , right: 12 }}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "month"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< YAxis
tickLine = { false }
axisLine = { false }
width = { 36 }
tickFormatter = {( v : number ) => `${ v / 1000 }k` }
/>
< ChartTooltip content = {< ChartTooltipContent indicator = "line" />} />
< ChartLegend content = {< ChartLegendContent />} />
{([ "referral" , "paid" , "organic" ] as const ). map (( key ) => (
< Area
key = {key}
dataKey = {key}
type = "monotone"
stackId = "traffic"
stroke = { `var(--color-${ key })` }
fill = { `var(--color-${ key })` }
fillOpacity = { 0.25 }
/>
))}
</ AreaChart >
</ ChartContainer >
);
}
Lines are best for trends and comparing a few series. Use dot={false} for dense data and type="monotone" for smooth curves that don't overshoot. Colors can come from any token, such as var(--destructive) for a critical series.
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ day: "Mon" , critical: 4 , high: 12 },
{ day: "Tue" , critical: 7 , high: 9 },
{ day: "Wed" , critical: 3 , high: 15 },
{ day: "Thu" , critical: 9 , high: 11 },
{ day: "Fri" , critical: 5 , high: 8 },
{ day: "Sat" , critical: 2 , high: 4 },
{ day: "Sun" , critical: 1 , high: 6 },
];
const config = {
critical: { label: "Critical" , color: "var(--destructive)" },
high: { label: "High" , color: "var(--chart-4)" },
} satisfies ChartConfig ;
export default function ChartLine () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[220px] w-full max-w-lg"
>
< LineChart data = {data} margin = {{ left: 0 , right: 12 }}>
< CartesianGrid vertical = { false } />
< XAxis dataKey = "day" tickLine = { false } axisLine = { false } tickMargin = { 8 } />
< YAxis tickLine = { false } axisLine = { false } width = { 28 } />
< ChartTooltip content = {< ChartTooltipContent />} />
< Line
dataKey = "critical"
type = "monotone"
stroke = "var(--color-critical)"
strokeWidth = { 2 }
dot = { false }
/>
< Line
dataKey = "high"
type = "monotone"
stroke = "var(--color-high)"
strokeWidth = { 2 }
dot = { false }
/>
</ LineChart >
</ ChartContainer >
);
}
For a pie, put the color on each data row as fill: "var(--color-<name>)" and pass nameKey so the tooltip and legend look up each slice's label in the config. hideLabel drops the tooltip's heading, which isn't meaningful for a single slice.
import { Pie, PieChart } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ 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 config = {
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 ;
export default function ChartPie () {
return (
< ChartContainer config = {config} className = "aspect-square h-[280px]" >
< PieChart >
< ChartTooltip
content = {< ChartTooltipContent nameKey = "browser" hideLabel />}
/>
< Pie data = {data} dataKey = "visitors" nameKey = "browser" />
< ChartLegend
content = {
< ChartLegendContent
nameKey = "browser"
className = "flex-wrap gap-x-4 gap-y-1"
/>
}
/>
</ PieChart >
</ ChartContainer >
);
}
innerRadius turns a pie into a donut. A Recharts Label with a content function draws the total in the middle using fill-foreground and fill-muted-foreground, so it follows the theme.
import { Label, Pie, PieChart } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ plan: "enterprise" , seats: 1240 , fill: "var(--color-enterprise)" },
{ plan: "team" , seats: 2860 , fill: "var(--color-team)" },
{ plan: "starter" , seats: 1910 , fill: "var(--color-starter)" },
];
const config = {
seats: { label: "Seats" },
enterprise: { label: "Enterprise" , color: "var(--chart-1)" },
team: { label: "Team" , color: "var(--chart-2)" },
starter: { label: "Starter" , color: "var(--chart-3)" },
} satisfies ChartConfig ;
const total = data. reduce (( sum , d ) => sum + d.seats, 0 );
export default function ChartDonut () {
return (
< ChartContainer config = {config} className = "aspect-square h-[240px]" >
< PieChart >
< ChartTooltip
cursor = { false }
content = {< ChartTooltipContent nameKey = "plan" hideLabel />}
/>
< Pie
data = {data}
dataKey = "seats"
nameKey = "plan"
innerRadius = { 64 }
strokeWidth = { 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 font-semibold text-2xl"
>
{total. toLocaleString ()}
</ tspan >
< tspan
x = {viewBox.cx}
y = {(viewBox.cy ?? 0 ) + 22 }
className = "fill-muted-foreground"
>
Paid seats
</ tspan >
</ text >
);
}}
/>
</ Pie >
</ PieChart >
</ ChartContainer >
);
}
ChartTooltipContent shows a label and one row per series. indicator picks a dot (default), line or dashed swatch; with a single series and line or dashed, the label moves into the row. hideLabel and hideIndicator remove those parts. These previews use Recharts' defaultIndex to show the tooltip without hovering.
import { Bar, BarChart, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ day: "Mon" , builds: 38 , failures: 4 },
{ day: "Tue" , builds: 52 , failures: 2 },
{ day: "Wed" , builds: 47 , failures: 6 },
{ day: "Thu" , builds: 61 , failures: 3 },
{ day: "Fri" , builds: 44 , failures: 1 },
];
const config = {
builds: { label: "Builds" , color: "var(--chart-1)" },
failures: { label: "Failures" , color: "var(--destructive)" },
} satisfies ChartConfig ;
const variants = [
{ title: 'indicator="dot"' , props: { indicator: "dot" } },
{ title: 'indicator="line"' , props: { indicator: "line" } },
{ title: 'indicator="dashed"' , props: { indicator: "dashed" } },
{ title: "hideLabel" , props: { hideLabel: true } },
] as const ;
export default function ChartTooltipExample () {
return (
< div className = "grid w-full max-w-2xl gap-6 sm:grid-cols-2" >
{variants. map (( v ) => (
< div key = {v.title} className = "flex flex-col gap-2" >
< code className = "text-muted-foreground text-xs" >{v.title}</ code >
< ChartContainer config = {config} className = "aspect-auto h-[180px]" >
< BarChart data = {data} margin = {{ top: 8 }}>
< XAxis
dataKey = "day"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< ChartTooltip
defaultIndex = { 1 }
cursor = { false }
content = {< ChartTooltipContent { ... v.props} />}
/>
< Bar dataKey = "builds" fill = "var(--color-builds)" radius = { 4 } />
< Bar dataKey = "failures" fill = "var(--color-failures)" radius = { 4 } />
</ BarChart >
</ ChartContainer >
</ div >
))}
</ div >
);
}
For custom content, labelFormatter(label, payload) replaces the heading and formatter(value, name, item, index) replaces an entire row. See the KPI recipe for currency formatting.
ChartLegendContent renders each series' config label with a color swatch, or the config icon when one is set (the icon also replaces the tooltip indicator). Place the legend above the chart with verticalAlign="top" on both ChartLegend and ChartLegendContent.
import { MonitorIcon, SmartphoneIcon, TabletIcon } from "lucide-react" ;
import { CartesianGrid, Line, LineChart, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ month: "Apr" , desktop: 186 , mobile: 120 , tablet: 40 },
{ month: "May" , desktop: 205 , mobile: 168 , tablet: 44 },
{ month: "Jun" , desktop: 237 , mobile: 190 , tablet: 52 },
{ month: "Jul" , desktop: 213 , mobile: 232 , tablet: 49 },
{ month: "Aug" , desktop: 256 , mobile: 261 , tablet: 58 },
{ month: "Sep" , desktop: 274 , mobile: 298 , tablet: 61 },
];
const config = {
desktop: { label: "Desktop" , icon: MonitorIcon, color: "var(--chart-1)" },
mobile: { label: "Mobile" , icon: SmartphoneIcon, color: "var(--chart-2)" },
tablet: { label: "Tablet" , icon: TabletIcon, color: "var(--chart-3)" },
} satisfies ChartConfig ;
export default function ChartLegendExample () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[260px] w-full max-w-lg"
>
< LineChart data = {data} margin = {{ left: 12 , right: 12 }}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "month"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< ChartTooltip content = {< ChartTooltipContent />} />
< ChartLegend
verticalAlign = "top"
content = {< ChartLegendContent verticalAlign = "top" />}
/>
{([ "desktop" , "mobile" , "tablet" ] as const ). map (( key ) => (
< Line
key = {key}
dataKey = {key}
type = "monotone"
stroke = { `var(--color-${ key })` }
strokeWidth = { 2 }
dot = { false }
/>
))}
</ LineChart >
</ ChartContainer >
);
}
color accepts any CSS color, including tokens like var(--chart-1) which already switch with the theme. For colors that need a different value per mode, use theme: { light, dark }; the container writes the dark values under the .dark class. Toggle the site theme to see this chart change.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ quarter: "Q1" , revenue: 182 , target: 200 },
{ quarter: "Q2" , revenue: 236 , target: 220 },
{ quarter: "Q3" , revenue: 271 , target: 250 },
{ quarter: "Q4" , revenue: 318 , target: 290 },
];
// `theme` sets a separate color for light and dark mode.
const config = {
revenue: {
label: "Revenue ($k)" ,
theme: { light: "oklch(0.55 0.17 160)" , dark: "oklch(0.78 0.15 160)" },
},
target: {
label: "Target ($k)" ,
theme: { light: "oklch(0.87 0.02 260)" , dark: "oklch(0.4 0.03 260)" },
},
} satisfies ChartConfig ;
export default function ChartTheme () {
return (
< ChartContainer
config = {config}
className = "aspect-auto h-[220px] w-full max-w-lg"
>
< BarChart data = {data} barGap = { 4 }>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "quarter"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
/>
< ChartTooltip content = {< ChartTooltipContent />} />
< Bar dataKey = "target" fill = "var(--color-target)" radius = { 4 } />
< Bar dataKey = "revenue" fill = "var(--color-revenue)" radius = { 4 } />
</ BarChart >
</ ChartContainer >
);
}
The default palette is --chart-1 to --chart-5 (indigo, teal, amber, pink, sky), defined for light and dark mode in the theme. Override those variables to re-color every chart at once.
A Card with a headline figure, a trend badge and an axis-less area sparkline flush with the card's bottom edge. The tooltip uses labelFormatter and formatter to show the date and a currency value.
import { TrendingUpIcon } from "lucide-react" ;
import { Area, AreaChart } from "recharts" ;
import { Badge } from "@/components/ui/badge" ;
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
const data = [
{ date: "Sep 1" , mrr: 38200 },
{ date: "Sep 5" , mrr: 39100 },
{ date: "Sep 9" , mrr: 38800 },
{ date: "Sep 13" , mrr: 40600 },
{ date: "Sep 17" , mrr: 41900 },
{ date: "Sep 21" , mrr: 41400 },
{ date: "Sep 25" , mrr: 43800 },
{ date: "Sep 29" , mrr: 45250 },
];
const config = {
mrr: { label: "MRR" , color: "var(--chart-1)" },
} satisfies ChartConfig ;
const currency = new Intl. NumberFormat ( "en-US" , {
style: "currency" ,
currency: "USD" ,
maximumFractionDigits: 0 ,
});
export default function ChartRecipeKpi () {
return (
< Card className = "w-full max-w-sm overflow-hidden pb-0" >
< CardHeader >
< CardDescription >Monthly recurring revenue</ CardDescription >
< CardTitle className = "font-semibold text-2xl tabular-nums" >
{currency. format ( 45250 )}
</ CardTitle >
< CardAction >
< Badge size = "sm" color = "success" >
< TrendingUpIcon /> +18.5%
</ Badge >
</ CardAction >
</ CardHeader >
< CardContent className = "px-0" >
< ChartContainer config = {config} className = "aspect-auto h-[96px] w-full" >
< AreaChart
data = {data}
margin = {{ top: 4 , left: 0 , right: 0 , bottom: 0 }}
>
< defs >
< linearGradient id = "kpiFill" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop
offset = "0%"
stopColor = "var(--color-mrr)"
stopOpacity = { 0.35 }
/>
< stop
offset = "100%"
stopColor = "var(--color-mrr)"
stopOpacity = { 0 }
/>
</ linearGradient >
</ defs >
< ChartTooltip
cursor = { false }
content = {
< ChartTooltipContent
labelFormatter = {( _ , payload ) =>
String (payload[ 0 ]?.payload?.date ?? "" )
}
formatter = {( value ) => (
< div className = "flex w-full justify-between gap-4" >
< span className = "text-muted-foreground" >MRR</ span >
< span className = "font-medium font-mono tabular-nums" >
{currency. format ( Number (value))}
</ span >
</ div >
)}
/>
}
/>
< Area
dataKey = "mrr"
type = "monotone"
stroke = "var(--color-mrr)"
strokeWidth = { 2 }
fill = "url(#kpiFill)"
/>
</ AreaChart >
</ ChartContainer >
</ CardContent >
</ Card >
);
}
A segmented ToggleButtonGroup in CardAction switches which series the bar chart shows, and the title shows the matching total. minTickGap thins the date ticks on narrow screens.
import { useState } from "react" ;
import type { Key } from "react-aria-components" ;
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" ;
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
import { ToggleButton } from "@/components/ui/toggle-button" ;
import { ToggleButtonGroup } from "@/components/ui/toggle-button-group" ;
const data = [
{ date: "Sep 16" , signups: 142 , activations: 88 },
{ date: "Sep 17" , signups: 168 , activations: 101 },
{ date: "Sep 18" , signups: 155 , activations: 97 },
{ date: "Sep 19" , signups: 201 , activations: 124 },
{ date: "Sep 20" , signups: 96 , activations: 58 },
{ date: "Sep 21" , signups: 84 , activations: 49 },
{ date: "Sep 22" , signups: 187 , activations: 119 },
{ date: "Sep 23" , signups: 214 , activations: 140 },
{ date: "Sep 24" , signups: 230 , activations: 151 },
{ date: "Sep 25" , signups: 198 , activations: 133 },
{ date: "Sep 26" , signups: 176 , activations: 115 },
{ date: "Sep 27" , signups: 103 , activations: 66 },
{ date: "Sep 28" , signups: 91 , activations: 57 },
{ date: "Sep 29" , signups: 242 , activations: 162 },
];
const config = {
signups: { label: "Sign-ups" , color: "var(--chart-1)" },
activations: { label: "Activations" , color: "var(--chart-2)" },
} satisfies ChartConfig ;
type Metric = keyof typeof config;
const totals = {
signups: data. reduce (( s , d ) => s + d.signups, 0 ),
activations: data. reduce (( s , d ) => s + d.activations, 0 ),
};
export default function ChartRecipeInteractive () {
const [ metric , setMetric ] = useState < Metric >( "signups" );
return (
< Card className = "w-full max-w-2xl" >
< CardHeader >
< CardTitle >
{totals[metric]. toLocaleString ()} {config[metric].label. toLowerCase ()}
</ CardTitle >
< CardDescription >Last 14 days</ CardDescription >
< CardAction >
< ToggleButtonGroup
aria-label = "Metric"
variant = "segmented"
size = "sm"
selectionMode = "single"
disallowEmptySelection
selectedKeys = {[metric]}
onSelectionChange = {( keys : Set < Key >) => {
const [ next ] = keys;
if (next) setMetric (next as Metric );
}}
>
< ToggleButton id = "signups" >Sign-ups</ ToggleButton >
< ToggleButton id = "activations" >Activations</ ToggleButton >
</ ToggleButtonGroup >
</ CardAction >
</ CardHeader >
< CardContent >
< ChartContainer
config = {config}
className = "aspect-auto h-[220px] w-full"
>
< BarChart data = {data}>
< CartesianGrid vertical = { false } />
< XAxis
dataKey = "date"
tickLine = { false }
axisLine = { false }
tickMargin = { 8 }
minTickGap = { 24 }
/>
< ChartTooltip content = {< ChartTooltipContent />} />
< Bar dataKey = {metric} fill = { `var(--color-${ metric })` } radius = { 4 } />
</ BarChart >
</ ChartContainer >
</ CardContent >
</ Card >
);
}
A small donut next to a custom legend list with percentages. The list sits outside ChartContainer, so its swatches use the config's color values rather than the scoped --color-* variables.
Search 50% Direct 25% Social 16% Email 9%
import { Pie, PieChart } from "recharts" ;
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card" ;
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart" ;
type Source = "search" | "direct" | "social" | "email" ;
const data : { source : Source ; visits : number ; fill : string }[] = [
{ source: "search" , visits: 18420 , fill: "var(--color-search)" },
{ source: "direct" , visits: 9310 , fill: "var(--color-direct)" },
{ source: "social" , visits: 5870 , fill: "var(--color-social)" },
{ source: "email" , visits: 3240 , fill: "var(--color-email)" },
];
const config = {
visits: { label: "Visits" },
search: { label: "Search" , color: "var(--chart-1)" },
direct: { label: "Direct" , color: "var(--chart-2)" },
social: { label: "Social" , color: "var(--chart-3)" },
email: { label: "Email" , color: "var(--chart-4)" },
} satisfies ChartConfig ;
const total = data. reduce (( sum , d ) => sum + d.visits, 0 );
export default function ChartRecipeTrafficSources () {
return (
< Card className = "w-full max-w-md" >
< CardHeader separator >
< CardTitle >Traffic sources</ CardTitle >
< CardDescription >
{total. toLocaleString ()} visits in September
</ CardDescription >
</ CardHeader >
< CardContent className = "flex items-center gap-6" >
< ChartContainer
config = {config}
className = "aspect-square h-[140px] shrink-0"
>
< PieChart >
< ChartTooltip
cursor = { false }
content = {< ChartTooltipContent nameKey = "source" hideLabel />}
/>
< Pie
data = {data}
dataKey = "visits"
nameKey = "source"
innerRadius = { 42 }
strokeWidth = { 3 }
/>
</ PieChart >
</ ChartContainer >
< ul className = "flex flex-1 flex-col gap-2.5 text-sm" >
{data. map (( d ) => (
< li key = {d.source} className = "flex items-center gap-2" >
< span
aria-hidden
className = "size-2.5 shrink-0 rounded-[2px]"
style = {{ backgroundColor: config[d.source].color }}
/>
< span className = "flex-1" >{config[d.source].label}</ span >
< span className = "font-mono text-muted-foreground tabular-nums" >
{Math. round ((d.visits / total) * 100 )}%
</ span >
</ li >
))}
</ ul >
</ CardContent >
</ Card >
);
}
Recharts 3 turns on its accessibilityLayer by default: the chart surface is focusable (tabIndex={0}) with role="application", and in bar, line and area charts ← and → move the tooltip between data points. Pass accessibilityLayer={false} to opt out.
Charts are visual. Always state the key takeaway in text nearby (a title, a total, a delta), and offer the underlying numbers as a table or download for anything important.
Don't rely on color alone to tell series apart. Use a legend with clear labels, direct labels, icons, or different line styles (for example strokeDasharray).
Keep enough contrast between adjacent series and against the background in both themes; the default --chart-* palette is tuned for this.
Tooltips appear on hover and keyboard focus only. Put anything essential in the visible chart or surrounding text.
With the default accessibility layer:
Key Action Tab Moves focus to the chart ← / → Moves the active tooltip to the previous or next data point (axis-based charts)
Attribute On Present when data-slot="chart"ChartContainerAlways data-chartChartContainerAlways. A unique id (chart-<id>) that scopes the generated --color-* variables. Pass id for a stable value.
Variable Defined for --color-<key>Every config key with a color or theme value, scoped to [data-chart=…], with dark values under .dark --chart-1 … --chart-5The theme's default chart palette (global)
Avoid config keys that match Tailwind color names used inside the chart (like border or muted), since the generated --color-<key> would shadow them within the container.
ChartContainer restyles Recharts' default greys to your tokens: axis tick text uses muted-foreground; the grid, polar grid, reference lines and line cursor use border; the bar cursor and radial bar backgrounds use muted. It also removes white strokes on dots and sectors and hides focus outlines on the SVG layers. Size the chart with className: the default is aspect-video; use aspect-auto h-[240px] for a fixed height, or aspect-square for pies.
Also accepts every prop of <div>.
Recharts passes active, payload and label automatically when used as content.
Only entries whose key is in the config get a label.
Recharts' Tooltip and Legend , re-exported unchanged. Useful props include cursor, defaultIndex and content on the tooltip, and verticalAlign, align and content on the legend.
const { config } = useChart ();
Reads the nearest ChartContainer's config. Throws when used outside one.
Card : the usual frame for a chart with a title and summary.
Meter : a single value within a range.
Toggle Button Group : switch series or time ranges.
Table : the accessible alternative for exact values.