# React Charts Tailwind: 4 Charts and a Kit for Any Theme

> React charts Tailwind can theme: area, bar, line and donut charts plus a chart kit on d3-scale and d3-shape, readable in light and dark mode and on a phone.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/react-charts-tailwind

## TL;DR

For React charts in a Tailwind app, pick SVG charts whose colors are CSS variables with separate light and dark values, whose readout pins above the finger on touch screens, and that ship a data table for screen readers. shadcn/ui charts and Tremor give you Tailwind theming on top of Recharts; the Wingo UI area, bar, line and donut charts draw directly with d3-scale and d3-shape, and the chart kit they share lets you build your own.

You are building a dashboard and you want React charts Tailwind can actually theme, so they look like the rest of the app. Tailwind classes do not reach inside most chart libraries: the colors live in a JavaScript config, the tooltip has its own font, dark mode turns the grid into bright lines, and on a phone the tooltip sits under your thumb. This list covers what React charts with Tailwind need to get right, how the common libraries handle it, and five Wingo UI pieces for a dashboard: the [Area Chart](https://wingo-ui.com/components/area-chart), [Bar Chart](https://wingo-ui.com/components/bar-chart), [Line Chart](https://wingo-ui.com/components/line-chart), [Donut Chart](https://wingo-ui.com/components/donut-chart) and the [Chart Kit](https://wingo-ui.com/components/chart-kit) under them, each with a demo or a snippet.

## What should React charts with Tailwind get right?

Six things, and the first two are about Tailwind specifically:

1. **Colors as tokens.** Every series color is a CSS variable, so it follows your theme like `bg-card` does, and nobody hardcodes `#8884d8` in a component.
2. **A designed dark mode.** Dark gets its own color steps. A blue that reads well on white is often too dark on near black, and an automatic flip does not fix that.
3. **A phone layout.** On a touch screen the readout pins above the plot instead of following a finger, a sideways drag scrubs through points, and a vertical drag still scrolls the page.
4. **Screen reader and keyboard access.** A chart is a picture of numbers, so it needs the numbers in text: a label, a short takeaway and a data table.
5. **No layout shift.** The chart reserves its height before it measures its width, so the page does not jump when it renders on the client.
6. **Number formatting.** Currency, compact ticks ("$128K") and dates go through `Intl`, with a `locale` prop.

Point 3 is the easiest to miss when you only test with a mouse on a wide screen: the tooltip you hover on a laptop ends up under the thumb that tapped it on a phone.

## Which React Tailwind chart library should you pick?

Most of the popular options build on Recharts, which itself draws with D3 modules. Facts below are from each project's docs and the npm registry, checked on October 19, 2026.

| Option | What it is | Engine | Theming | License |
|---|---|---|---|---|
| [shadcn/ui Chart](https://ui.shadcn.com/docs/components/chart) | Source components you copy with the shadcn CLI | Recharts v3 | `--chart-1` style CSS variables, or a config color with `light` and `dark` keys | MIT |
| [Tremor](https://www.tremor.so/) | Chart and dashboard components, copy and paste or the `@tremor/react` package (3.18.7, released January 13, 2025) | Recharts | Tailwind classes | Apache 2.0 |
| [Recharts](https://github.com/recharts/recharts) 3.10.1 | Composable React chart components | React and D3 (d3-scale, d3-shape and more through `victory-vendor`) | Props per element | MIT |
| [Chart.js](https://www.chartjs.org/docs/latest/) 4.5.1 | Chart library you wrap in React yourself | HTML5 canvas | Colors set in JavaScript options; Tailwind classes cannot style the canvas | MIT |
| [Wingo UI charts](https://wingo-ui.com/components/area-chart) | Source components, `npx wingo-ui@latest add area-chart` | SVG drawn with d3-scale and d3-shape, motion for animation | `--series-1` to `--series-8` with separate dark values, tones such as `"success"` | Commercial (Pro plan) |

Recharts is a solid default, and both shadcn/ui and Tremor build on it. Since version 3.0 its [release notes](https://github.com/recharts/recharts/releases/tag/v3.0.0) say the accessibility layer is on by default on cartesian and polar charts, so keyboard and screen reader support come built in. If your team already knows its API, stay on it. A Recharts alternative pays off when you want different defaults for phones and dark mode and want to own the chart source. The shadcn chart docs note that the container needs a `min-h-[VALUE]` class to be responsive, which is worth knowing before you ship one.

Chart.js is the one to think twice about in a Tailwind app. Its colors are JavaScript options drawn onto a canvas, so dark mode means reading token values with `getComputedStyle` and redrawing when the theme changes.

## How are d3 React charts different from Recharts?

They are one layer thinner. In d3 React charts, `d3-scale` turns values into pixels and `d3-shape` turns points into an SVG path string; React renders the `<path>` and owns the DOM. Recharts does the same math inside its own components, and you configure them with props.

The Wingo UI charts use that thin approach: no Recharts, no `d3-selection`, nothing that touches the DOM outside React. The practical difference for you is that the source you copy is the chart itself, so changing how a tooltip or a bar end looks means editing a file you own. The [d3-shape docs](https://d3js.org/d3-shape) describe the same pattern: a line generator's output goes straight into a path's `d` attribute.

## 1. Area chart: how do I show revenue by channel over time?

Use stacked areas when the series add up to a total, overlapping areas when you compare two volumes, and percent when you care about shares. The [Area Chart](https://wingo-ui.com/components/area-chart) does all three with one `type` prop, and switching between them glides the bands to their new shape.

```bash
npx wingo-ui@latest add area-chart
```

```tsx
import { AreaChart } from "@/components/ui/area-chart";

const data = [
  { month: "2026-07", direct: 41460, online: 28700, partners: 18070 },
  { month: "2026-08", direct: 38030, online: 27300, partners: 15600 },
  { month: "2026-09", direct: 48920, online: 36400, partners: 22130 },
];

export function RevenueByChannel() {
  return (
    <AreaChart
      data={data}
      xKey="month"
      series={[
        { key: "direct", label: "Direct sales" },
        { key: "online", label: "Online store" },
        { key: "partners", label: "Partners" },
      ]}
      type="stacked"
      valueFormat="currency"
      currency="USD"
      locale="en-US"
      label="Revenue by channel, 2026"
    />
  );
}
```

ISO months such as `"2026-09"` print as "Sep" on the axis and "September 2026" in the readout. The readout of a stacked chart lists the top band first and adds a Total row; a percent chart shows each value with its share. `fill` switches between `"gradient"` (the default), `"solid"` and `"none"`, and `referenceLines` draws dashed targets.

## 2. Bar chart: how do I compare months or categories?

Use bars for discrete periods and categories. The [Bar Chart](https://wingo-ui.com/components/bar-chart) is `grouped` by default, and also does `stacked` and `percent`. Negative values grow down from a clear zero line, and `getBarColor` paints a bar by its value:

```tsx
import { BarChart } from "@/components/ui/bar-chart";

export function CashFlow({ data }: { data: { month: string; change: number }[] }) {
  return (
    <BarChart
      data={data}
      xKey="month"
      series={[{ key: "change", label: "Change" }]}
      getBarColor={(row) => ((row.change as number) < 0 ? "danger" : "success")}
      showValues
      valueFormat="currency"
      currency="USD"
      locale="en-US"
      label="Monthly cash-flow change, 2026"
      description="Three months had a negative cash flow."
    />
  );
}
```

`"danger"` and `"success"` are tone names, so the bars use your status tokens in both themes. Value labels from `showValues` switch to the compact form ("$128K") when full amounts do not fit, and a label with no room moves inside its bar or is skipped, never clipped. For long category names on a phone, set `orientation="horizontal"`: the names move into a label column on the left and the tooltip keeps the full text.

Hover or tap a month in the demo, then switch to Stacked and 100% to see the bars move.

> Live demo (Bar chart): Hover or tap a month to read both series, switch between Grouped, Stacked and 100% in the header, and click a legend entry to hide a series. Try it at [Bar chart](https://wingo-ui.com/components/bar-chart) and install it with `npx wingo-ui@latest add bar-chart`.

## 3. Line chart: how do I show a trend against last year?

Use a line for a trend over many points, and a dashed second series for the comparison period. In the [Line Chart](https://wingo-ui.com/components/line-chart), a series with `dashed: true` draws at 70% opacity with a muted row in the readout.

```tsx
import { LineChart } from "@/components/ui/line-chart";

const data = [
  { month: "2026-07", revenue: 106300, lastYear: 91200 },
  { month: "2026-08", revenue: 97500, lastYear: 88400 },
  { month: "2026-09", revenue: 128450, lastYear: 104700 },
];

export function RevenueTrend() {
  return (
    <LineChart
      data={data}
      xKey="month"
      series={[
        { key: "revenue", label: "2026" },
        { key: "lastYear", label: "2025", dashed: true },
      ]}
      valueFormat="currency"
      locale="en-US"
      referenceLines={[{ value: 120000, label: "Target $120,000" }]}
      endLabels
      label="Revenue 2026 compared with 2025"
    />
  );
}
```

`endLabels` puts the last value and the series name at the end of each line. Under 360px it keeps only the short value and a legend names the lines, and a label that would collide is dropped, never nudged on top of another. For a calm single KPI line, pass `colors={["neutral"]}` with `showArea` and `showDots`. To line up two charts stacked on top of each other, give both the same `yAxisWidth` and share one `activeIndex`, so the crosshair moves in both.

Switch to Compare 2025 in the demo and move along the line to read both years.

> Live demo (Line chart): Switch the header to Compare 2025 to add the dashed line for last year, then hover, tap or drag sideways across the plot to read both years at each month. Try it at [Line chart](https://wingo-ui.com/components/line-chart) and install it with `npx wingo-ui@latest add line-chart`.

## 4. Donut chart: how do I show parts of a whole?

Use a donut when there are a handful of parts and the total matters. The [Donut Chart](https://wingo-ui.com/components/donut-chart) shows the total in the middle and a legend with values and shares, and it folds anything past `maxSlices` (6 by default) into one gray Other slice whose readout lists what is inside.

```tsx
import { DonutChart } from "@/components/ui/donut-chart";

const channels = [
  { id: "direct", label: "Direct sales", value: 48920 },
  { id: "online", label: "Online store", value: 36400 },
  { id: "partners", label: "Partners", value: 22130 },
  { id: "tenders", label: "Public tenders", value: 13600 },
];

export function ChannelMix() {
  return <DonutChart data={channels} valueFormat="currency" locale="en-US" label="Revenue by channel, September 2026" />;
}
```

Slices are sorted biggest first by default (`sort="none"` keeps your order). Select a slice and it stays lifted while the center reads its value, name and share; `value` and `onValueChange` make that selection controlled, so a list next to the chart can drive it. The legend sits beside the chart from a 480px wide container and below it on narrower ones. That is a container query, so it is right in the server HTML and in a narrow dashboard column on a desktop.

The ring's hit area is widened by 12px on each side for both mouse and touch, so thin rings and small slices are easy to hit. On a phone, sliding a finger along the ring scrubs through the slices, and the scrub ends by selecting the slice it rests on.

> Live demo (Donut chart): Hover or tap a slice to read it in the center, click it to keep it selected, and switch the month in the header to watch the slices glide. Try it at [Donut chart](https://wingo-ui.com/components/donut-chart) and install it with `npx wingo-ui@latest add donut-chart`.

## 5. Chart kit: how do I build a chart type that is not in the list?

Use the [Chart Kit](https://wingo-ui.com/components/chart-kit), the shared layer the four charts are built on. It exports the palette, formatters, a measuring container, scales, axes, grid, the crosshair inspector, tooltip, legend, the screen reader table and the loading and empty states. A custom chart composes those parts and gets the same touch, keyboard and theme behavior:

```tsx
import {
  ChartContainer,
  ChartSvg,
  ChartTooltip,
  ChartInspectLayer,
  chartLayout,
  niceScale,
  valueDomain,
  useChartInspect,
  formatChartValue,
} from "@/lib/chart-kit";

const data = [
  { month: "Jul", revenue: 106300 },
  { month: "Aug", revenue: 97500 },
  { month: "Sep", revenue: 128450 },
];

function Bars({ width, height }: { width: number; height: number }) {
  const layout = chartLayout({ width, height, showYAxis: false });
  const { scale: y } = niceScale(valueDomain(data.map((d) => d.revenue)), [layout.bottom, layout.top]);
  const step = layout.innerWidth / data.length;
  const positions = data.map((_, i) => layout.left + step * (i + 0.5));
  const inspect = useChartInspect({ positions });
  const i = inspect.activeIndex;

  return (
    <>
      <ChartSvg width={width} height={height}>
        {data.map((d, index) => (
          <rect key={d.month} x={positions[index] - 12} width={24} y={y(d.revenue)} height={y(0) - y(d.revenue)} rx={4} fill="var(--series-1)" />
        ))}
      </ChartSvg>
      <ChartInspectLayer controller={inspect} label="Revenue by month" />
      <ChartTooltip
        x={i == null ? null : positions[i]}
        title={i == null ? "" : data[i].month}
        items={
          i == null
            ? []
            : [{ key: "v", label: "Revenue", value: formatChartValue(data[i].revenue, { format: "currency", locale: "en-US", currency: "USD" }), color: "var(--series-1)", shape: "rect" }]
        }
      />
    </>
  );
}

export function RevenueBars() {
  return (
    <ChartContainer aria-label="Revenue by month" height={200}>
      {(size) => <Bars {...size} />}
    </ChartContainer>
  );
}
```

`ChartContainer` reserves the 200px before it measures, then calls your function with the real width and height. `niceScale` returns a linear scale with about one tick per 56px, and `getCurve` and `stackData` wrap the d3-shape curves and stacks when you need lines or bands. The kit is how the library's other charts, such as the [Sparkline](https://wingo-ui.com/components/sparkline) and the [Gauge](https://wingo-ui.com/components/gauge), stay consistent with these four.

## How do the charts handle dark mode?

Good React charts dark mode support starts with colors that have their own dark values. The Wingo UI charts have eight palette slots. `ChartContainer` sets `--series-1` to `--series-8` on the figure, and the dark theme swaps them for separately chosen steps instead of inverting the light ones. The comment in the source says the order was checked so neighboring colors stay apart for the common kinds of color blindness in both themes. A ninth series never gets a new hue; it becomes "other", a gray mixed from your foreground color.

Every `color` and `colors` prop takes three kinds of values: a slot (`"series-3"`), a tone (`"neutral"`, `"brand"`, `"success"`, `"danger"`, mapped to your status tokens) or any CSS color. Grid lines are a faint foreground tint and axis labels use the muted text color, so they recede in both themes. Gaps between bar segments and the rings around dots are painted in the `surface` color, which defaults to `var(--surface)`. Set it when the chart sits on a different fill, for example `surface="var(--card)"`, so the gaps match what is behind them. The [Tailwind v4 dark mode guide](https://wingo-ui.com/blog/tailwind-v4-dark-mode) covers the class toggle and token setup these variables rely on.

## How do the charts work on a phone?

The readout moves out from under your finger. `tooltipMode="auto"` floats the readout beside the crosshair with a mouse and pins it above the plot on touch screens. Tap a point to pin it, drag sideways to scrub, and tap again or anywhere outside to clear. The plot layer only claims horizontal drags, so a vertical swipe still scrolls the page.

X axis labels are thinned from the newest one backwards, so the latest month always shows and labels never overlap at 360px. Legends wrap, and legend entries are real buttons with a larger hit area on coarse pointers. For a dashboard that also runs on phones, the [Next.js admin dashboard tutorial](https://wingo-ui.com/blog/nextjs-admin-dashboard-tutorial) shows these charts inside a full app shell.

## Are the charts accessible?

Each chart is an element with `role="figure"` and an accessible name from `label` and an optional one-sentence takeaway from `description`, read through `aria-describedby`. Every chart also renders a data table that is hidden from sight and present in the server HTML, so a screen reader or a crawler gets the numbers without running JavaScript. With the keyboard, Tab to the plot, then Left and Right walk the points (or the slices of a donut), Home and End jump to the ends, and Escape clears, while a live region reads each point. Enter fires `onDatumClick` on the area, bar and line charts and selects a slice on the donut.

Areas rise, lines draw in and slices sweep the first time a chart scrolls into view. With reduced motion turned on in the operating system, the final chart shows at once ([how reduced motion works in React](https://wingo-ui.com/blog/prefers-reduced-motion-react)).

## How do I add the charts to my project?

Install the ones you need:

```bash
npx wingo-ui@latest add area-chart bar-chart line-chart donut-chart
```

The CLI copies the charts into `components/ui`, adds the chart kit as `lib/chart-kit.tsx` along with the hooks and helpers the charts import, and installs `d3-scale`, `d3-shape` and `motion`. The charts are Pro items ([pricing](https://wingo-ui.com/pricing)). For the full picture of libraries for dashboards, read the [best React component library for dashboard apps](https://wingo-ui.com/blog/best-react-component-library-for-dashboard) comparison, and the [component guides category](https://wingo-ui.com/blog/category/components) has more single-component deep dives, starting with the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide).

## Components in this post

- [Area chart](https://wingo-ui.com/components/area-chart) (Pro): Volumes over time as filled areas: overlapping gradients, stacked bands for parts of a total, or 100% stacked shares. Install: `npx wingo-ui@latest add area-chart`
- [Bar chart](https://wingo-ui.com/components/bar-chart) (Pro): Compares categories or periods as columns or horizontal bars, grouped, stacked or 100%, with negatives below a clear zero line. Install: `npx wingo-ui@latest add bar-chart`
- [Line chart](https://wingo-ui.com/components/line-chart) (Pro): Trends over time as smooth lines, with a crosshair readout of every series, a dashed comparison period, target lines and direct end labels. Install: `npx wingo-ui@latest add line-chart`
- [Donut chart](https://wingo-ui.com/components/donut-chart) (Pro): Parts of a whole as a donut or pie, with the total in the middle, a legend with values and shares, and a selectable slice. Install: `npx wingo-ui@latest add donut-chart`
- [Chart kit](https://wingo-ui.com/components/chart-kit) (Pro): A React chart toolkit: the palette, formatters, measuring container, axes, crosshair, tooltip, legend and data table charts share. Install: `npx wingo-ui@latest add chart-kit`

## FAQ

### What is the best chart library for React and Tailwind?

If you already use shadcn/ui, its chart component on Recharts v3 is the low-friction choice, and Tremor offers a similar Tailwind look, also on Recharts. If you want charts that are already designed for phones and dark mode, with the source in your repository, the Wingo UI charts are an option.

### How do I make React charts work in dark mode?

Use an SVG chart library and give every series color as a CSS variable, with its own value in the dark theme instead of an automatic inversion. Canvas libraries such as Chart.js take their colors from JavaScript options, so you read the token values yourself and redraw on a theme switch.

### Is there a Recharts alternative that uses Tailwind?

Tremor and shadcn/ui use Tailwind but still run on Recharts. The Wingo UI charts drop Recharts: SVG drawn with d3-scale and d3-shape, styled with Tailwind tokens, copied into your project as source.

### Can I use d3 with React without touching the DOM?

Yes. Use d3-scale to map values to pixels and d3-shape to build path strings, then render the SVG with JSX. React owns the DOM and d3 only does the math, which is how the Wingo UI chart kit works.

### Are the Wingo UI charts free?

No. The area, bar, line and donut charts and the chart kit are Pro items. As of October 2026, Pro costs $8 a month, $80 a year or $150 once.

---

Source: https://wingo-ui.com/blog/react-charts-tailwind
