Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/radar-chart.jsonUsage
import { RadarChart } from "@/components/dithercn/radar-chart";
export function Evals() {
return (
<RadarChart
axes={["Reasoning", "Coding", "Math", "Writing", "Vision"]}
series={[
{ label: "Model v3", values: [86, 91, 78, 72, 64] },
{ label: "Model v2", values: [71, 74, 69, 76, 40] },
]}
max={100}
/>
);
}Axes run clockwise from 12 o'clock. Each series lists one value per axis in the same order. Without max, the outer ring sits at a round number above the largest value.
Every series is a polygon of whole dither cells with a solid one-cell outline and square markers on its corners. With fill="gradient" the density steps up once per ring, thin at the centre and dense near the rim, so the grid reads straight through the shape. Each series takes the next palette colour. Where series overlap their cells interleave, so neither hides the other, and every outline stays on top.
Move the pointer around the chart and the readout snaps to the nearest axis, with every series' value there. Focus the chart and the arrow keys step around the axes. Hover a legend entry to spotlight a series, click to keep it lit, and press Escape to let go.
The chart fills its container up to size and shrinks with it, keeping room for the labels.
Examples
Single series in a card
One series in green with fill="dotted" and a round grid. The legend hides itself when there is only one series.
Comparing plans
Three series on seven axes with fill="hatched", five rings and bloom="high". Hatching keeps overlapping shapes readable because each one's lines run between the other's.
Props
| Prop | Type | Default |
|---|---|---|
| axes* Axis labels, clockwise from the top. Three or more read best. | Type: [string, ...string[]] | Default – |
| series* { label: string; values: number[]; color?: DitherColor; fill?: FillVariant }, one value per axis. | Type: [RadarSeries, ...RadarSeries[]] | Default – |
| max The value at the outer ring. | Type: number | Default A round number above the largest value |
| rings Grid rings. They also set the steps of the gradient fill. | Type: number | Default 4 |
| grid Shape of the grid rings. | Type: "polygon" | "circle" | Default "polygon" |
| fill Texture of every series. A series' own fill wins. | Type: "gradient" | "dotted" | "hatched" | "solid" | Default "gradient" |
| bloom Soft glow in each series' colour behind it. | Type: "off" | "low" | "high" | Default "off" |
| colors Colours by series position. A series' own color wins. | Type: DitherColor[] | Default – |
| size Largest width and height in CSS pixels. It shrinks to fit its container. | Type: number | Default 360 |
| format How values are written in the readout and the data table. | Type: Intl.NumberFormatOptions | ((value: number) => string) | Default Compact, as in 1.2K |
| legend Show the legend. | Type: boolean | Default With two or more series |
| interactive The axis scrub, keyboard and legend spotlight. false draws a static image. | Type: boolean | Default true |
| animate Grow out from the centre the first time the chart is seen. | Type: boolean | Default true |
| matrix Threshold pattern of the gradient fill. | Type: "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "bayer4" |
| cell Size of one dither cell in CSS pixels. | Type: number | Default --dither-cell |
Notes
- The chart is a focusable group with a summary label. The arrow keys move between axes, a live region reads each axis's values, and a hidden table lists every value.
- The shapes grow out from the centre once when the chart scrolls into view. With reduced motion they appear at once and the readout moves without easing.
- The drawing is one SVG path per series, built from whole cells snapped to device pixels. Scrubbing only moves the readout by transform and restyles one spoke.