
Recharts ships functional charts out of the box, but its defaults look nothing like the rest of a Tailwind-designed site — different fonts, hard-coded colors, a tooltip that clashes with everything around it. Getting it to feel native to the design system is mostly about three things: making it a Client Component correctly, wiring its colors to your Tailwind/CSS-variable theme instead of hex literals, and replacing the default tooltip and legend with your own markup.
1. It has to be a Client Component
Recharts measures its container and renders SVG using browser APIs, so any component that renders a chart needs "use client" — but that doesn't mean the whole page does. Fetch the data in a Server Component parent and pass it down as a prop.
// Server Component — fetches data, stays on the server
import { SkillsChart } from "./skills-chart";
export default async function SkillsSection() {
const skills = await skillRepository.findAllWithLevels();
return <SkillsChart data={skills} />; // only this leaf needs "use client"
}2. Theming with CSS variables instead of hex literals
Recharts components accept a stroke/fill prop as any valid CSS color — including var(--some-token). Reading colors straight from the site's Tailwind theme means the chart re-themes itself automatically in dark mode instead of needing a separate light/dark color map maintained by hand.
// globals.css (already defines the design tokens used everywhere else)
:root {
--chart-1: oklch(0.65 0.22 260);
--chart-2: oklch(0.7 0.18 160);
--chart-grid: oklch(0.9 0 0);
--chart-text: oklch(0.45 0 0);
}
.dark {
--chart-1: oklch(0.75 0.2 260);
--chart-2: oklch(0.78 0.16 160);
--chart-grid: oklch(0.3 0 0);
--chart-text: oklch(0.7 0 0);
}"use client";
import {
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from "recharts";
export function SkillsChart({ data }: { data: { name: string; level: number }[] }) {
return (
<ResponsiveContainer width="100%" height={320}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
<XAxis dataKey="name" stroke="var(--chart-text)" fontSize={12} tickLine={false} />
<YAxis stroke="var(--chart-text)" fontSize={12} tickLine={false} />
<Tooltip content={<ChartTooltip />} cursor={{ fill: "var(--chart-grid)", opacity: 0.3 }} />
<Bar dataKey="level" fill="var(--chart-1)" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}Give ResponsiveContainer an explicit height (a percentage height alone can render as 0px inside a flex/grid parent, since Recharts measures against the parent's resolved pixel height) — this is the single most common "my chart doesn't show up" bug.
3. A tooltip that actually matches the design
The default tooltip is a plain white box with inline styles. Passing your own component to content gives you a normal Tailwind-styled element instead.
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="rounded-lg border bg-popover px-3 py-2 text-sm shadow-md">
<p className="font-medium text-popover-foreground">{label}</p>
{payload.map((entry: any) => (
<p key={entry.dataKey} className="text-muted-foreground">
{entry.name}: <span className="text-foreground">{entry.value}</span>
</p>
))}
</div>
);
}4. Multi-series charts and a custom legend
<LineChart data={monthlyViews}>
<Line type="monotone" dataKey="en" stroke="var(--chart-1)" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="fa" stroke="var(--chart-2)" strokeWidth={2} dot={false} />
<Legend content={<ChartLegend />} />
</LineChart>function ChartLegend({ payload }: any) {
return (
<div className="mt-3 flex justify-center gap-4 text-sm">
{payload.map((entry: any) => (
<span key={entry.value} className="flex items-center gap-1.5">
<span className="size-2.5 rounded-full" style={{ background: entry.color }} />
{entry.value}
</span>
))}
</div>
);
}5. RTL and this site's Persian locale
Recharts doesn't auto-flip for RTL layouts — an axis order that reads naturally in English can read backwards in Persian. Reverse the data order (or set reversed on the axis) when the active locale is Farsi, rather than trying to mirror the SVG itself.
const locale = useLocale(); // next-intl
const chartData = locale === "fa" ? [...data].reverse() : data;Checklist
Chart component is a small Client Component leaf; data fetching stays in a Server Component parent.
Colors read from CSS variables tied to the Tailwind theme, not hardcoded hex — dark mode then needs zero extra chart-specific logic.
ResponsiveContainer has an explicit pixel height, not just width="100%".
Tooltip and legend replaced with real Tailwind markup instead of Recharts' default inline-styled versions.
Data order reversed for RTL locales where axis direction actually matters.
More posts

Authentication with better-auth in Next.js: A First-Hand Setup
The exact setup behind this site's own admin login: server config, the Next.js catch-all route handler, the client, and the two-layer middleware + layout protection that actually secures /admin.

Tiptap in Next.js: Editable Admin Panel, Read-Only Public Render
The exact Tiptap setup behind every post on this site: one shared extension list for the editor and generateHTML(), storing JSON in Postgres, syntax-highlighted code blocks, and uploaded (not base64) images.