Documentation
Charts
Compose accessible, theme-aware charts by wiring recharts into ChartContainer with a typed ChartConfig that maps each series to a Cooud token.
The chart primitives
A chart is a recharts graph composed inside ChartContainer. You give the container a ChartConfig — a map from each series key to a label and a color, usually a var(--cooud-*) token. The container exposes those colors to its recharts children as var(--color-<key>) custom properties, so series, tooltips, and legends all recolor together when the theme changes.
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@cooud-ui/ui";import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";// The config maps each series key to a label and a theme token. The token is// exposed to recharts children as a `var(--color-<key>)` custom property.const chartConfig = {revenue: { label: "Revenue", color: "var(--cooud-primary)" },profit: { label: "Profit", color: "var(--cooud-info)" },} satisfies ChartConfig;export function Chart() {return (<ChartContainer config={chartConfig} className="h-64 w-full"><BarChart accessibilityLayer data={chartData}>{/* recharts components compose inside the container and read colors via var(--color-*). */}<Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} /></BarChart></ChartContainer>);}
recharts is a peer dependency, imported directly alongside @cooud-ui/ui. The container ships ChartTooltip, ChartTooltipContent, ChartLegend, and ChartLegendContent alongside it. See the Chart component for the full API.
A bar chart
Compose a recharts BarChart inside ChartContainer. The ChartConfig maps each series to a theme token, exposed to the bars as var(--color-revenue) and var(--color-profit). CartesianGrid, XAxis, and the tooltip all live inside the same container.
const chartConfig = {revenue: { label: "Revenue", color: "var(--cooud-primary)" },profit: { label: "Profit", color: "var(--cooud-info)" },} satisfies ChartConfig;const chartData = [{ month: "Jan", revenue: 4200, profit: 1400 },{ month: "Feb", revenue: 3800, profit: 1100 },{ month: "Mar", revenue: 5200, profit: 1900 },{ month: "Apr", revenue: 4900, profit: 1700 },{ month: "May", revenue: 6100, profit: 2400 },{ month: "Jun", revenue: 7300, profit: 3100 },];return (<ChartContainer config={chartConfig} className="h-64 w-full"><BarChart accessibilityLayer data={chartData}><CartesianGrid vertical={false} /><XAxis dataKey="month" tickLine={false} tickMargin={10} axisLine={false} /><ChartTooltipcursor={{ fill: "var(--cooud-fg)", fillOpacity: 0.05, radius: 8 }}content={<ChartTooltipContent />}/><Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} /><Bar dataKey="profit" fill="var(--color-profit)" radius={4} /></BarChart></ChartContainer>);
Donut, radar & radial
The same ChartContainer + ChartConfig pattern drives every recharts chart family. Swap the recharts subtree for a PieChart, RadarChart, or RadialBarChart — each slice or shape reads its fill from a var(--color-<key>) token. These three demos wrap the chart in the accessible role='img' summary described below.
Donut chart
const chartConfig = {visitors: { label: "Visitors" },direct: { label: "Direct", color: "var(--cooud-chart-1)" },organic: { label: "Organic", color: "var(--cooud-chart-2)" },referral: { label: "Referral", color: "var(--cooud-chart-3)" },social: { label: "Social", color: "var(--cooud-chart-4)" },email: { label: "Email", color: "var(--cooud-chart-5)" },} satisfies ChartConfig;const chartData = [{ source: "direct", visitors: 4200, fill: "var(--color-direct)" },{ source: "organic", visitors: 3100, fill: "var(--color-organic)" },{ source: "referral", visitors: 1900, fill: "var(--color-referral)" },{ source: "social", visitors: 1400, fill: "var(--color-social)" },{ source: "email", visitors: 800, fill: "var(--color-email)" },];return (// Data is exposed to assistive tech via the label; the SVG internals (recharts// tags each sector role="img" with no name) are hidden so they're not announced.<divrole="img"aria-label="Donut chart of traffic sources by visitors: Direct 4,200, Organic 3,100, Referral 1,900, Social 1,400, Email 800."className="h-64 w-full"><div aria-hidden="true" className="h-full w-full"><ChartContainer config={chartConfig} className="h-full w-full"><PieChart><ChartTooltip content={<ChartTooltipContent hideLabel />} /><Piedata={chartData}dataKey="visitors"nameKey="source"innerRadius={64}outerRadius={98}paddingAngle={3}cornerRadius={6}strokeWidth={0}rootTabIndex={-1}>{chartData.map((entry) => (<Cell key={entry.source} fill={entry.fill} />))}<Labelcontent={({ viewBox }) => {if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) return null;const { cx, cy } = viewBox;return (<text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle"><tspan x={cx} y={cy} className="fill-fg font-semibold text-2xl">11.4K</tspan><tspan x={cx} y={(cy ?? 0) + 22} className="fill-fg-tertiary text-xs">Visitors</tspan></text>);}}/></Pie><ChartLegend content={<ChartLegendContent nameKey="source" />} /></PieChart></ChartContainer></div></div>);
Radar chart
const chartConfig = {current: { label: "Current", color: "var(--cooud-primary)" },target: { label: "Target", color: "var(--cooud-info)" },} satisfies ChartConfig;const chartData = [{ metric: "Speed", current: 86, target: 95 },{ metric: "Reliability", current: 72, target: 90 },{ metric: "Coverage", current: 91, target: 88 },{ metric: "Security", current: 78, target: 96 },{ metric: "Usability", current: 84, target: 80 },{ metric: "Support", current: 69, target: 85 },];return (<divrole="img"aria-label="Radar chart comparing Current vs Target across six metrics — Speed 86/95, Reliability 72/90, Coverage 91/88, Security 78/96, Usability 84/80, Support 69/85."className="h-64 w-full"><div aria-hidden="true" className="h-full w-full"><ChartContainer config={chartConfig} className="h-full w-full"><RadarChart data={chartData}><ChartTooltip content={<ChartTooltipContent />} /><PolarGrid /><PolarAngleAxis dataKey="metric" /><Radar dataKey="current" fill="var(--color-current)" fillOpacity={0.6} stroke="var(--color-current)" /><Radar dataKey="target" fill="var(--color-target)" fillOpacity={0.15} stroke="var(--color-target)" /><ChartLegend content={<ChartLegendContent />} /></RadarChart></ChartContainer></div></div>);
Radial bar gauge
const chartConfig = {visitors: { label: "Visitors" },desktop: { label: "Desktop", color: "var(--cooud-chart-1)" },mobile: { label: "Mobile", color: "var(--cooud-chart-2)" },tablet: { label: "Tablet", color: "var(--cooud-chart-3)" },other: { label: "Other", color: "var(--cooud-chart-4)" },} satisfies ChartConfig;const chartData = [{ device: "desktop", visitors: 5200, fill: "var(--color-desktop)" },{ device: "mobile", visitors: 4100, fill: "var(--color-mobile)" },{ device: "tablet", visitors: 1800, fill: "var(--color-tablet)" },{ device: "other", visitors: 900, fill: "var(--color-other)" },];return (<divrole="img"aria-label="Radial bar chart of visitors by device: Desktop 5,200, Mobile 4,100, Tablet 1,800, Other 900."className="h-64 w-full"><div aria-hidden="true" className="h-full w-full"><ChartContainer config={chartConfig} className="h-full w-full"><RadialBarChart data={chartData} innerRadius={32} outerRadius={110}><ChartTooltip content={<ChartTooltipContent hideLabel nameKey="device" />} /><PolarGrid gridType="circle" radialLines={false} stroke="none" /><RadialBar dataKey="visitors" background cornerRadius={8} /><ChartLegend content={<ChartLegendContent nameKey="device" />} /></RadialBarChart></ChartContainer></div></div>);
Tooltip & legend
ChartTooltip and ChartLegend are recharts' Tooltip and Legend with Cooud-styled content. Pass the provided content components and they read each series' label and color indicator straight from the ChartConfig, so you never duplicate the series metadata.
// The tooltip and legend read their labels from the same ChartConfig the// container was given — pass the provided content components and they resolve// each series key to its config `label` (and color indicator) automatically.<ChartContainer config={chartConfig} className="h-64 w-full"><BarChart accessibilityLayer data={chartData}><ChartTooltip content={<ChartTooltipContent />} /><ChartLegend content={<ChartLegendContent />} /><Bar dataKey="revenue" fill="var(--color-revenue)" radius={4} /></BarChart></ChartContainer>
The config is the single source of truth
ChartTooltipContent and ChartLegendContent resolve a series key to its label in the config. Use nameKey when the legend or tooltip should key off a data field (like source or device) instead of the series dataKey.Accessibility & code-splitting
recharts tags every sector with role='img' and no accessible name, so a screen reader hears a run of nameless images. The demos wrap each chart in a single role='img' element carrying an aria-label that summarizes the data, then mark the recharts SVG subtree aria-hidden so it isn't announced.
// recharts tags every sector with role="img" and no accessible name, so a screen// reader would announce a pile of nameless images. Wrap the chart in a single// role="img" with an aria-label summary, then hide the SVG subtree from a11y.<divrole="img"aria-label="Donut chart of traffic sources by visitors: Direct 4,200, Organic 3,100, Referral 1,900, Social 1,400, Email 800."className="h-64 w-full"><div aria-hidden="true" className="h-full w-full"><ChartContainer config={chartConfig} className="h-full w-full">{/* recharts subtree — hidden from assistive tech, summarized above. */}</ChartContainer></div></div>
recharts is heavy and purely client-visual, so the demos load it through next/dynamic with ssr: false. The chart stays out of the first-load JS of any route that doesn't draw one, streaming in behind a height-matched skeleton so there's no layout shift.
import dynamic from "next/dynamic";// recharts is heavy and purely client-visual, so the chart render is split into// its own client-only chunk and pulled in via next/dynamic (ssr: false). It// never enters the first-load JS of routes that don't draw a chart.const RevenueChart = dynamic(() => import("./revenue-chart"), {ssr: false,loading: () => (<div className="h-64 w-full animate-pulse rounded-lg border border-border bg-surface-inset/50" aria-hidden="true" />),});
Summarize the data, hide the SVG
aria-label and mark the chart subtree aria-hidden="true". For interactive charts, set rootTabIndex={-1} on the recharts root (for example <Pie>) so the hidden subtree stays out of the tab order.