Skip to content

Chart

Stable

Themed, accessible charts built on Recharts, with a validated categorical palette, quiet chrome and tooltips that match the system.

Visitors this week27,390

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/chart.json

Usage

A chart is Recharts inside a ChartContainer. The container takes a config that names each series and assigns its color; every series color becomes a CSS variable, --color-<key>, scoped to that chart.

import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from "@/components/ui/chart"

const config = {
  visitors: { label: "Visitors", color: "var(--chart-1)" },
} satisfies ChartConfig

<ChartContainer config={config} className="aspect-auto h-64 w-full">
  <AreaChart data={data}>
    <CartesianGrid vertical={false} />
    <XAxis dataKey="day" tickLine={false} axisLine={false} />
    <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
    <Area
      dataKey="visitors"
      stroke="var(--color-visitors)"
      strokeWidth={2}
      fill="var(--color-visitors)"
      fillOpacity={0.1}
    />
  </AreaChart>
</ChartContainer>

Give the container an explicit height that includes the axis labels — aspect-auto h-64 — so the axes never get clipped into a nested scroll.

Examples

Area

A single series tells one story, so it needs no legend: the title names it. Draw a 2px line with a 10% wash of the same color underneath — a quiet fill, never a solid block.

API requests
Monthly requests, in millions, over the last 12 months.
Up 108% year over year.

Line

With two or more series, a legend is always present. Label the end of each line with its latest value instead of putting a number on every point.

Weekly active users by platformLast 8 weeks

Bar

Bars are at most 24px thick, grow from one baseline, and round only their data end (4px). A 2px gap separates bars that touch.

Sign-ups and activationsAccounts per month, 2026

Stacked

For part-to-whole comparisons across many categories, stack horizontally. A 2px stroke in the surface color separates segments — the gap does the separating, not an outline.

Build minutes by environmentSeptember 2026, per team

Donut

Donuts show part-to-whole at a glance for up to six segments. For comparing close values, use a bar chart — angles are hard to compare.

Traffic by source

Stat with sparkline

Often the right chart is a number. A stat tile leads with the value, states the change against a named period, and adds a 12-point sparkline: history in a quiet gray, the current period in the accent. Color the change by whether it's good news, and pair it with an arrow so direction never depends on color.

Recurring revenue$84,320+8.2%vs. August
p95 latency182 ms−24 msvs. August
Error rate0.42%+0.09 ptvs. August

Emphasis

When the point is one series, say so: show it in the accent and everything else in gray. This is the most underused chart form, and often the honest answer to "make this chart clearer".

Sydney waits 5.6× longer than FrankfurtMedian latency from Kraków, in milliseconds

With a table view

Every chart should have a table twin. It's the most accessible form of the data, the one people copy from, and it keeps values reachable without hovering.

Support tickets per quarter

Color

Color comes last: pick the form first, then give each color exactly one job.

JobEncodesprfct colors
CategoricalWhich serieschart-1chart-5, always assigned in order
SequentialHow muchOne hue, light → dark: brand steps 4 → 11
DivergingAbove or below a baselineBlue and red scales with a gray midpoint
StatusGood or badsuccess, warning, destructive — only when the series means good or bad
EmphasisThe one that matterschart-1 for the subject, gray-8 for context

Categorical palette

chart-1
chart-2
chart-3
chart-4
chart-5

The order — brand, teal, violet, orange, pink — isn't aesthetic. It was chosen by enumerating every ordering and keeping the one with the largest color difference between neighbors under simulated protanopia and deuteranopia. Every step also sits in the legible lightness band, clears the chroma floor, and keeps at least 3:1 contrast with the chart surface, in both modes:

CheckLightDark
Worst adjacent pair, color-blind (ΔE OKLab)17.716.0
Worst adjacent pair, normal vision (ΔE OKLab)18.716.8
Contrast against the surface≥ 3:1≥ 3:1

Rules that follow from this:

  • Assign in order, never skip, never cycle. The first series is always chart-1. A sixth series folds into Other, becomes a small multiple, or moves to a table — never a generated sixth hue.
  • Color follows the entity, not its rank. When a filter removes a series, the others keep their colors.
  • Scatter plots and maps get three series at most — there, any two colors can end up side by side.
  • Status colors are reserved. Green and red mean good and bad everywhere in prfct; never use them for "series 4".

Guidelines

Choose the form first

The data's jobUse
One current value, maybe a trendA stat tile — not a one-bar chart
Change over timeLine; area for a single series
Compare magnitudesBar or column
Part-to-wholeStacked bar; a donut for up to six parts
One series matters, the rest is contextEmphasis
More than seven categories that all matterA table

Quiet chrome

Data is the only thing allowed to be loud. Gridlines are solid hairlines one step off the surface — never dashed. Axis labels use muted-foreground. Remove axis lines and tick marks; keep ticks at clean, comma-separated numbers.

Do.Hairline grid, muted labels, thin marks. The data carries the ink.
Don’t.A different color per bar spends the identity channel on nothing — the length already shows the value.

One axis

Never plot two measures on two y-axes: the alignment of the scales is arbitrary, so the chart invents correlations. Use two charts, small multiples, or index both series to 100 at the start.

Labels and legends

  • A legend is always present for two or more series; a single series is named by the title.
  • Label selectively — the latest value, the extreme, the one series the story is about. Never a number on every point.
  • Text wears text tokens. Values, labels and legends stay in foreground or muted-foreground; the colored mark beside them carries identity.

Accessibility

  • Keyboard. Recharts' accessibility layer is on by default: focus a chart with Tab and move between data points with the arrow keys; the tooltip follows focus exactly as it follows the pointer.
  • A text alternative. Pair every chart with a table view, or describe the takeaway in its title and caption — "Signups grew 64% since April" says more than "Signups by month".
  • Never color alone. Legends and direct labels identify series; status changes pair color with an icon and words.
  • Contrast. Every categorical color keeps at least 3:1 against the chart surface in both modes.
  • Motion. Recharts animations default to "auto", which respects prefers-reduced-motion. Keep it.
KeyBehavior
Tab
Moves focus to the chart.
Moves between data points and updates the tooltip.

API reference

ChartContainer

Wraps a Recharts chart, scopes its colors and makes it responsive. Accepts all div props.

PropTypeDefault
config

Labels, icons and colors for each series key. Every color becomes --color-key inside the chart.

ChartConfigNo default
initialDimension

Size used before the container is measured, for example during server rendering.

{ width: number; height: number }{ width: 320, height: 200 }
className

Set a height here, e.g. aspect-auto h-64. The default is a 16:9 aspect ratio.

stringNo default

ChartConfig

type ChartConfig = Record<
  string,
  {
    label?: React.ReactNode
    icon?: React.ComponentType
  } & (
    | { color?: string } // one color for both modes
    | { theme: { light: string; dark: string } } // a color per mode
  )
>

ChartTooltipContent

Use as <ChartTooltip content={<ChartTooltipContent />} />.

PropTypeDefault
indicator

Series key style. Prefer line for line and area charts.

"dot" | "line" | "dashed""dot"
hideLabel

Hides the category label at the top of the tooltip.

booleanfalse
hideIndicator

Hides the series key, e.g. for single-series charts.

booleanfalse
nameKey

Payload key used to look up the series label in config.

stringNo default
labelKey

Payload key used for the tooltip label.

stringNo default
formatter

Replaces the default row rendering.

(value, name, item, index, payload) => ReactNodeNo default

ChartLegendContent

Use as <ChartLegend content={<ChartLegendContent />} />.

PropTypeDefault
nameKey

Payload key used to look up labels, e.g. for pie slices.

stringNo default
hideIcon

Hides config icons and shows color keys instead.

booleanfalse
verticalAlign

Adds spacing on the side facing the chart.

"top" | "bottom""bottom"

ChartStyle

Injected by ChartContainer; exported for custom containers that need the same scoped color variables.