Skip to main content

SVG Charts in React: Create Dynamic Graphics

SVG (Scalable Vector Graphics) is the native browser format for drawing shapes—circles, lines, polygons, paths—that scale infinitely without pixelation. When you pair SVG with React's component model, you get interactive graphics that update in milliseconds as data or interactivity changes. This article teaches you to build animated SVG charts from scratch, compose them as React components, and optimize them for responsiveness.

What Makes SVG Perfect for React Charts?

SVG charts render as DOM elements (<svg>, <circle>, <path>), making them inspectable, accessible, and easy to style with CSS or React props. Unlike Canvas (which is a bitmap), SVG is vector-based: a chart drawn at 400x300px renders identically at 1200x900px without pixelation. This is critical for responsive dashboards.

SVG also natively supports events: you can attach click handlers, hover listeners, and drag callbacks to individual shapes. A Canvas bitmap doesn't know where shapes are; you must calculate collision detection manually. For interactive charts, SVG's DOM integration is a huge advantage.

The downside: SVG struggles with very large datasets (10,000+ points) because each shape is a DOM node. Canvas handles millions of pixels per frame effortlessly. But for typical dashboards, SVG is superior.

Building Your First SVG Chart: A Simple Line Chart

Let's build a basic line chart rendering sales data as an SVG path. The key steps are: (1) compute scales, (2) generate SVG path data, (3) render as a React component.

import React from "react";
import { scaleTime, scaleLinear } from "d3-scale";

const SalesLineChart = ({ data }) => {
// Sample data: [{ date: Date, sales: number }, ...]
// Margins for axes and labels
const margin = { top: 20, right: 30, bottom: 30, left: 60 };
const width = 600;
const height = 400;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

// Extract domain from data
const dates = data.map((d) => d.date);
const sales = data.map((d) => d.sales);

// Scales: transform data to pixel coordinates
const xScale = scaleTime()
.domain([Math.min(...dates), Math.max(...dates)])
.range([0, innerWidth]);

const yScale = scaleLinear()
.domain([0, Math.max(...sales)])
.range([innerHeight, 0]); // inverted: top = 0px, bottom = innerHeight

// Build SVG path data: "M 10,20 L 30,40 L 50,60 ..."
let pathData = "";
data.forEach((d, i) => {
const x = xScale(d.date);
const y = yScale(d.sales);
pathData += (i === 0 ? "M" : "L") + ` ${x},${y}`;
});

return (
<svg
width={width}
height={height}
style={{ border: "1px solid #ccc" }}
>
{/* Background group (plot area) */}
<g transform={`translate(${margin.left}, ${margin.top})`}>
{/* Grid lines (optional) */}
{yScale.ticks(5).map((tick, i) => (
<line
key={i}
x1="0"
y1={yScale(tick)}
x2={innerWidth}
y2={yScale(tick)}
stroke="#e0e0e0"
strokeDasharray="4"
/>
))}

{/* The line path */}
<path
d={pathData}
stroke="steelblue"
strokeWidth="2"
fill="none"
/>

{/* Data points */}
{data.map((d, i) => (
<circle
key={i}
cx={xScale(d.date)}
cy={yScale(d.sales)}
r="4"
fill="steelblue"
/>
))}
</g>

{/* X axis */}
<g transform={`translate(${margin.left}, ${height - margin.bottom})`}>
<line x1="0" y1="0" x2={innerWidth} y2="0" stroke="black" />
{xScale.ticks(5).map((tick, i) => (
<g key={i} transform={`translate(${xScale(tick)}, 0)`}>
<line y1="0" y2="5" stroke="black" />
<text y="20" textAnchor="middle" fontSize="12">
{tick.toLocaleDateString()}
</text>
</g>
))}
</g>

{/* Y axis */}
<g transform={`translate(${margin.left}, ${margin.top})`}>
<line x1="0" y1="0" x2="0" y2={innerHeight} stroke="black" />
{yScale.ticks(5).map((tick, i) => (
<g key={i} transform={`translate(0, ${yScale(tick)})`}>
<line x1="-5" x2="0" stroke="black" />
<text x="-10" textAnchor="end" fontSize="12" dy="0.32em">
{tick}
</text>
</g>
))}
</g>
</svg>
);
};

export default SalesLineChart;

This chart is fully functional: zoom it, inspect the SVG in DevTools, and it's accessible. The path is computed once per render, making updates smooth.

Responsive SVG with viewBox

Hardcoded widths and heights don't work on mobile. Use SVG's viewBox attribute to scale the entire chart to fit its container.

import React, { useState, useEffect } from "react";

const ResponsiveSVGChart = ({ data }) => {
const [size, setSize] = useState({ width: 600, height: 400 });

useEffect(() => {
const handleResize = () => {
const container = document.getElementById("chart-container");
if (container) {
setSize({
width: container.clientWidth,
height: container.clientHeight
});
}
};

window.addEventListener("resize", handleResize);
handleResize(); // initial size
return () => window.removeEventListener("resize", handleResize);
}, []);

// Instead of width/height props, use viewBox
return (
<div id="chart-container" style={{ width: "100%", height: "400px" }}>
<svg
viewBox={`0 0 ${size.width} ${size.height}`}
preserveAspectRatio="xMidYMid meet"
style={{ width: "100%", height: "100%" }}
>
{/* Chart content scales automatically */}
{/* ... paths, circles, axes ... */}
</svg>
</div>
);
};

The viewBox tells the SVG to scale its internal coordinate system to fit the viewport. preserveAspectRatio="xMidYMid meet" keeps aspect ratio and centers the chart. This is responsive charting 101.

Animated SVG Chart Transitions

SVG elements can animate with CSS transitions, making them snappy without JavaScript overhead.

export function AnimatedBarChart({ data }) {
const [animationKey, setAnimationKey] = useState(0);

const handleDataUpdate = (newData) => {
// Trigger animation by changing a key (forces re-render)
setAnimationKey((k) => k + 1);
};

const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 600;
const height = 300;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

const xScale = scaleBand()
.domain(data.map((d) => d.label))
.range([0, innerWidth])
.padding(0.2);

const yScale = scaleLinear()
.domain([0, Math.max(...data.map((d) => d.value))])
.range([innerHeight, 0]);

return (
<svg width={width} height={height}>
<g transform={`translate(${margin.left}, ${margin.top})`}>
{data.map((d, i) => (
<rect
key={`${animationKey}-${i}`}
x={xScale(d.label)}
y={yScale(d.value)}
width={xScale.bandwidth()}
height={innerHeight - yScale(d.value)}
fill="teal"
style={{
transition: "all 0.3s ease", // smooth animation
opacity: 1
}}
/>
))}
</g>
</svg>
);
}

The CSS transition: "all 0.3s ease" animates position, height, and fill changes. When data updates, React computes new scales and the browser smoothly interpolates from old to new positions over 0.3 seconds.

Common SVG Gotchas and Fixes

Gotcha 1: Text positioning. SVG text doesn't center like HTML. Use textAnchor="middle" for horizontal centering and dy="0.32em" for vertical alignment.

Gotcha 2: Path data precision. SVG paths are space-sensitive: M10,20L30,40 is valid but M 10 , 20 L 30 , 40 might not be. Use templates:

// ✗ Don't do this
const pathData = "M " + x1 + "," + y1 + " L " + x2 + "," + y2;

// ✓ Better
const pathData = `M ${x1},${y1} L ${x2},${y2}`;

Gotcha 3: Transform stacking. Multiple transform attributes don't stack; use a single string:

// ✗ Wrong
<g transform="translate(50, 0)" transform="rotate(45)">

// ✓ Right
<g transform="translate(50, 0) rotate(45)">

Key Takeaways

  • SVG charts are DOM-based, making them interactive, inspectable, and accessible unlike Canvas.
  • Use D3 scales to map data to pixel coordinates, then render SVG paths and shapes.
  • viewBox and responsive containers make charts work on all screen sizes.
  • CSS transitions animate chart updates smoothly without JavaScript.
  • Master margins, path syntax, and transform stacking to avoid subtle bugs.

Frequently Asked Questions

Can I use CSS-in-JS libraries like styled-components with SVG charts?

Yes. SVG elements accept style props just like HTML. You can wrap your SVG in a styled component or use Tailwind classes on the container. Avoid over-complicated styling; SVG attributes are often clearer.

How do I make SVG charts accessible?

Add <title> and <desc> elements inside your SVG, use ARIA labels, and ensure axes have readable text. For keyboard navigation, consider adding tabindex and event listeners to interactive elements.

What's the maximum number of elements in an SVG before performance degrades?

In modern browsers, 5,000-10,000 elements stay interactive. Beyond that, consider Canvas, aggregation (draw every Nth point), or server-side rendering (send pre-rendered SVG). Profile with DevTools.

Can I export SVG charts as PNG or PDF?

Yes. Libraries like svg-to-image or canvas-based exports work. For PDFs, use libraries like jsPDF with html2canvas. Alternatively, right-click the SVG in the browser and "Save As."

Should I use React's dangerouslySetInnerHTML to inject SVG?

No. Build SVG elements as React components with proper props. dangerouslySetInnerHTML loses interactivity and makes refactoring harder. Exception: if you're rendering pre-generated SVG strings from a server (rare).

Further Reading