How to build an animated SVG radar chart in React
An animated SVG radar chart in React draws concentric polygon grids and axis lines with pure SVG, then plots each dataset as a `<path>` element whose `pathLength` animates from 0 to 1 via Framer Motion's `whileInView`. Data points appear as staggered `<circle>` elements after the path finishes drawing.
- Stack: React + Framer Motion 11 + CSS custom properties, ~135 lines, zero charting library.
- SVG-only geometry: polar-to-cartesian conversion with Math.cos/sin, no canvas, no D3.
- Requires at least 3 axes; returns null below that threshold to prevent degenerate shapes.
- Accessible: SVG text labels are real DOM text nodes, readable by screen readers.
- Responsive on mobile via the flex column layout, though the fixed 300×300 SVG does not rescale, consider a viewBox-based approach for smaller screens.
Comparison Radar Chart renders a pure-SVG spider/radar chart that compares two or more options across multiple scored dimensions. It skips charting libraries entirely: polygon grids, axis lines, and data polygons are all computed with basic trigonometry at render time. Framer Motion handles the scroll-triggered draw animation, turning raw numbers into a section visitors read in seconds.
Anatomy
The component wraps a centered 300×300 SVG inside a flex column that also holds a legend row below. Inside the SVG, four concentric polygons form the grid background, each at 25/50/75/100% of the radius. Thin radial lines connect the center to each axis tip. On top of those, one `<motion.path>` per option draws the filled data polygon. Staggered `<motion.circle>` elements mark each data point. Text labels sit 24px beyond the radius tips, computed at the same polar angle as their axis.
How it works
The key primitives are `polarToXY` (converts a 0-360 degree angle and radius to x/y SVG coordinates, offset by -90° so the first axis points up) and `buildPath` (maps each value to its point and joins them with M/L SVG path commands). Each `<motion.path>` animates `pathLength` from 0 to 1 via `whileInView`, with a 0.3s delay stagger between options. The data circles use a secondary stagger: `0.5 + optionIndex * 0.3 + axisIndex * 0.05` seconds, so they appear to pop in along the path as it draws. The easing curve `[0.16, 1, 0.3, 1]` is a custom cubic-bezier that gives a fast-out-slow-in feel throughout.
How to build it in React
Set up polar geometry helpers
Write two pure functions: `polarToXY` converts a degree angle and radius to SVG x/y coordinates (subtract 90° so 0° points up), and `buildPath` maps an array of 0-100 values to a closed SVG path string. Both are plain JavaScript, no library needed.
function polarToXY(angle: number, r: number): [number, number] { const rad = ((angle - 90) * Math.PI) / 180; return [CENTER + r * Math.cos(rad), CENTER + r * Math.sin(rad)]; } function buildPath(values: number[], count: number): string { return values .map((v, i) => { const angle = (360 / count) * i; const [x, y] = polarToXY(angle, (v / 100) * RADIUS); return `${i === 0 ? "M" : "L"}${x},${y}`; }) .join(" ") + " Z"; }Draw the grid and axis lines
Render 4 concentric `<polygon>` elements by computing each vertex at `(level / LEVELS) * RADIUS` along each axis angle. Then add one `<line>` per axis from center to its tip. Both use the `--color-border` CSS variable so they adapt to any theme preset automatically.
{Array.from({ length: LEVELS }).map((_, li) => { const r = ((li + 1) / LEVELS) * RADIUS; const pts = axes .map((_, ai) => polarToXY((360 / count) * ai, r).join(",")) .join(" "); return <polygon key={li} points={pts} fill="none" stroke="var(--color-border)" strokeWidth="1" opacity={0.5} />; })}Animate data polygons with pathLength
For each dataset, render a `<motion.path>` with the result of `buildPath`. Animate `pathLength` from 0 to 1 with `whileInView` so the draw triggers on scroll. Stagger multiple options with a `delay: index * 0.3` so they appear sequentially rather than all at once.
<motion.path d={buildPath(opt.values, count)} fill="var(--color-accent)" fillOpacity={0.15} stroke="var(--color-accent)" strokeWidth={2} initial={{ opacity: 0, pathLength: 0 }} whileInView={{ opacity: 1, pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: index * 0.3, ease: [0.16, 1, 0.3, 1] }} />Add staggered data point circles and labels
Map over each value in each option to render a `<motion.circle>` at the computed polar position. Use a compound delay `0.5 + optionIndex * 0.3 + axisIndex * 0.05` so circles appear to travel along the path. Place `<text>` elements 24px past the radius tip for axis labels, using `textAnchor='middle'` and `dominantBaseline='central'` so they center correctly regardless of angle.
When to use it
Reach for a radar chart when you need to compare 2-3 options across 5-8 qualitative or scored dimensions simultaneously, feature comparisons on SaaS pricing pages, candidate skill profiles, or product specification sheets. Skip it when your data is ordinal or sequential (a bar or line chart is clearer), when you have more than 3 options (the overlapping polygons become unreadable), or when you need precise numeric values (radar charts emphasise shape over numbers).
Used by
- GitHub, Uses radar-style skill visualisations in developer profile and contribution analytics features.
- Notion, Third-party Notion dashboards frequently embed radar charts for team skill matrices and OKR tracking.
- HubSpot, Contact and deal analytics dashboards use spider/radar charts to display multi-attribute lead scores at a glance.
FAQ
Why not use Chart.js or Recharts instead of raw SVG?
Chart.js adds ~60 KB and Recharts ~150 KB to your bundle. For a single radar chart on a marketing page, the two helper functions shown here cover 100% of the functionality without the weight. Use a charting library if you need dynamic data updates, tooltips, or a dashboard with many chart types.
How do I make the SVG scale responsively?
Remove the fixed `width` and `height` attributes from the `<svg>` element, set `width='100%'` and keep `viewBox='0 0 300 300'`. The browser will then scale the SVG proportionally to its container. You may need to increase the label font size slightly since it will be relative to the 300-unit viewBox, not the rendered size.
Can I add hover tooltips to each data point?
Yes. Replace the `<motion.circle>` elements with a `<g>` that wraps the circle and a conditionally rendered `<foreignObject>` containing a React tooltip, or use a `title` child element for a native SVG tooltip. For production, a small tooltip library like Floating UI is cleaner than managing SVG coordinate math for the tooltip position.
The pathLength animation flickers in Safari. How do I fix it?
Safari has known issues with SVG `pathLength` combined with `stroke-dasharray`. Add `pathLength={1}` as a static prop to the underlying `<path>` to force the browser to normalise the dash offsets, then let Framer Motion animate from 0 to 1. If that still flickers, animate `opacity` and `scale` instead as a fallback, less dramatic but universally supported.