Recharts in a single HTML file, without a build step

Recharts is a chart library written for React. In a plain HTML file you load React and Recharts from a CDN with four script tags, then draw into a div.

Recharts is a chart library built with React and D3 that draws its charts as SVG. It is normally installed with npm, but it also ships a script-tag build.

So one HTML file can load React and Recharts from a CDN and draw a chart with no build step. Hover the line below.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recharts in one HTML file</title>
<style>
  body { margin: 0; padding: 14px 16px; font-family: system-ui, sans-serif; background: #fff; color: #1d2330; }
  h1 { font-size: 16px; margin: 0 0 2px; }
  p { margin: 0 0 10px; font-size: 13px; color: #5b6472; }
  /* ResponsiveContainer fills its parent, so the parent needs a real height */
  #chart { height: 240px; }
</style>
</head>
<body>
<h1>Weekly sign-ups</h1>
<p>Hover or tap the line to read a value.</p>
<div id="chart"></div>

<!-- order matters: React, ReactDOM, ReactIs, then Recharts -->
<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-is@18.3.1/umd/react-is.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/recharts@3.10.1/umd/Recharts.js"></script>
<script>
  const h = React.createElement;  // no JSX in a plain script
  const { ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } = Recharts;

  const data = [
    { week: 'W1', signups: 12 }, { week: 'W2', signups: 19 },
    { week: 'W3', signups: 15 }, { week: 'W4', signups: 28 },
    { week: 'W5', signups: 34 }, { week: 'W6', signups: 31 },
  ];

  const chart = h(ResponsiveContainer, { width: '100%', height: '100%' },
    h(LineChart, { data, margin: { top: 8, right: 12, bottom: 0, left: -12 } },
      h(CartesianGrid, { strokeDasharray: '3 3', vertical: false }),
      h(XAxis, { dataKey: 'week' }),
      h(YAxis),
      h(Tooltip),
      h(Line, { dataKey: 'signups', stroke: '#2563eb', strokeWidth: 2.5 })
    )
  );

  ReactDOM.createRoot(document.getElementById('chart')).render(chart);
</script>
</body>
</html>
A Recharts line chart in one HTML file. Hover or tap the line to read a value.

The rest of this page is what that file contains, and the few mistakes that leave you with a blank box.

The four script tags

Recharts is a React component library, so it needs React on the page first. The script-tag build looks for three globals when it loads, and it is the order of the tags that supplies them.

Left: React, ReactDOM and react-is load before Recharts. Right: Recharts first, which fails.
Left: React, ReactDOM and react-is load before Recharts. Right: Recharts first, which fails.
  1. React – the core library, which provides window.React.
  2. ReactDOM – draws React output into the page, as window.ReactDOM.
  3. react-is – a helper library that Recharts lists as a peer dependency, as window.ReactIs.
  4. Recharts – provides window.Recharts with every component.

A classic script tag runs as soon as it is fetched, and the browser reads the tags in page order. So the Recharts tag must come after the other three, and your own script must come after Recharts.

Pin a version in each address, as in recharts@3.10.1. An address without a version follows the newest release, and the jsDelivr documentation does not recommend that for production.

Draw the chart with createElement

Recharts charts are trees of components: a chart holds axes, a grid, a tooltip and one component per data series. In a plain script you build that tree with React.createElement. Giving it a short name keeps the code readable.

const h = React.createElement;
const { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip } = Recharts;

const chart = h(ResponsiveContainer, { width: '100%', height: '100%' },
  h(LineChart, { data },
    h(XAxis, { dataKey: 'week' }),
    h(YAxis),
    h(Tooltip),
    h(Line, { dataKey: 'signups' })));

ReactDOM.createRoot(document.getElementById('chart')).render(chart);

The data is an array of objects, and each dataKey names a property. createRoot hands the div to React, and render draws the tree into it. For a bar or area chart, swap in BarChart with Bar, or AreaChart with Area.

Why the chart is blank: the parent needs a height

ResponsiveContainer takes a width and a height, either numbers or percentages such as '100%'. The documentation says it watches the parent's size with the ResizeObserver API, so the parent needs dimensions of its own.

The same chart in two parents. The one with a height draws, the one without gets zero pixels.
The same chart in two parents. The one with a height draws, the one without gets zero pixels.

A div with no content and no height measures zero pixels tall. Then 100% of zero is zero, the chart has nowhere to draw, and the page shows nothing and reports no error.

Try it: untick the box, and drag the width slider to see the chart follow its parent.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recharts parent size</title>
<style>
  body { margin: 0; padding: 14px 16px; font-family: system-ui, sans-serif; background: #fff; color: #1d2330; }
  .ctl { display: grid; gap: 10px; margin-bottom: 12px; font-size: 14px; }
  .ctl label { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  input[type=range] { flex: 1; min-width: 120px; }
  #readout { font: 13px ui-monospace, Consolas, monospace; background: #f4f5f7; border-radius: 8px; padding: 8px 10px; margin-bottom: 10px; }
  #readout.bad { background: #fff1e6; color: #9a3412; }
  #readout.good { background: #e8f6ee; color: #0f5132; }
  #stage { border: 1px dashed #aab2bf; border-radius: 8px; padding: 6px; }
  #box { height: 220px; }  /* the parent: toggled by the checkbox */
  #box.nohigh { height: auto; }
</style>
</head>
<body>
<div class="ctl">
  <label><input type="checkbox" id="hasHeight" checked> Parent has a height (220px)</label>
  <label>Parent width <input type="range" id="w" min="30" max="100" value="100"> <b id="wv">100%</b></label>
</div>
<div id="readout"></div>
<div id="stage"><div id="box"><div id="chart" style="height:100%"></div></div></div>

<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-is@18.3.1/umd/react-is.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/recharts@3.10.1/umd/Recharts.js"></script>
<script>
  const h = React.createElement;
  const { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } = Recharts;
  const data = [{ m: 'Jan', v: 40 }, { m: 'Feb', v: 55 }, { m: 'Mar', v: 32 }, { m: 'Apr', v: 70 }, { m: 'May', v: 61 }];

  ReactDOM.createRoot(document.getElementById('chart')).render(
    h(ResponsiveContainer, { width: '100%', height: '100%' },
      h(BarChart, { data, margin: { left: -16, right: 8, top: 8 } },
        h(XAxis, { dataKey: 'm' }), h(YAxis), h(Tooltip),
        h(Bar, { dataKey: 'v', fill: '#2563eb', radius: [4, 4, 0, 0] })))
  );

  const box = document.getElementById('box');
  const readout = document.getElementById('readout');
  const hasHeight = document.getElementById('hasHeight');
  const w = document.getElementById('w');

  // report what the chart's parent measures right now
  function report() {
    const r = box.getBoundingClientRect();
    const wh = Math.round(r.width) + ' x ' + Math.round(r.height) + ' px';
    const zero = r.height < 10;
    readout.textContent = 'Parent size: ' + wh + (zero ? '  -> nothing to draw in' : '  -> chart fills it');
    readout.className = zero ? 'bad' : 'good';
  }
  new ResizeObserver(report).observe(box);

  hasHeight.addEventListener('change', () => box.classList.toggle('nohigh', !hasHeight.checked));
  w.addEventListener('input', () => {
    box.style.width = w.value + '%';
    document.getElementById('wv').textContent = w.value + '%';
  });
  report();
</script>
</body>
</html>
Untick the height box to watch the chart disappear. The readout shows what the parent measures.

Two fixes work. Give the parent a height in CSS, or skip the container and put a fixed width and height on the chart. Recharts 3 also lets a chart take responsive with a CSS width and aspect ratio.

JSX does not run in a plain script

Most Recharts examples online are written in JSX, the HTML-like syntax inside JavaScript. A browser cannot read it, so pasting such an example into a script tag throws a syntax error.

Three ways to write the same chart. Plain JSX fails, createElement or Babel works.
Three ways to write the same chart. Plain JSX fails, createElement or Babel works.

The examples here avoid the problem with createElement. If you prefer JSX, load @babel/standalone and mark your script type="text/babel". Babel's documentation says it compiles those scripts in the browser, and that production sites should compile ahead of time instead.

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.29.0/babel.min.js"></script>
<script type="text/babel">
  const { LineChart, Line } = Recharts;
  ReactDOM.createRoot(document.getElementById('chart')).render(
    <LineChart width={400} height={200} data={data}>
      <Line dataKey="signups" />
    </LineChart>
  );
</script>

Why the examples use React 18

React stopped publishing UMD builds with React 19, and its upgrade guide suggests an ES-module CDN instead. These examples use the script-tag route, so they pin React 18.3.1, a release that still ships a UMD file.

Recharts lists React 16.8 up to 19 as supported peers, so React 18.3.1 is within range. If you later move the project to npm and a bundler, you can use whichever React version Recharts supports.

A finished page: paste numbers, get a chart

Charts get useful when the data is easy to change. This page reads comma-separated rows from a textarea, skips any row that is not all numbers, and redraws as a line, bar or area chart.

The form is handled inside the page and sends nothing anywhere.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recharts data explorer</title>
<style>
  body { margin: 0; padding: 14px 16px; font-family: system-ui, sans-serif; background: #fff; color: #1d2330; }
  h1 { font-size: 16px; margin: 0 0 8px; }
  textarea { width: 100%; box-sizing: border-box; height: 96px; font: 13px ui-monospace, Consolas, monospace; padding: 8px; border: 1px solid #c9ced8; border-radius: 8px; resize: vertical; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 8px 0; }
  button { font: inherit; font-size: 14px; padding: 6px 12px; border: 1px solid #c9ced8; background: #fff; border-radius: 8px; cursor: pointer; }
  button[aria-pressed=true] { background: #2563eb; border-color: #2563eb; color: #fff; }
  .spacer { flex: 1; }
  label { font-size: 14px; display: inline-flex; align-items: center; gap: 4px; }
  #msg { font-size: 13px; min-height: 18px; color: #9a3412; margin-bottom: 6px; }
  #chart { height: 260px; }
</style>
</head>
<body>
<h1>Paste numbers, get a chart</h1>
<form id="form">
  <textarea id="csv" aria-label="Data as CSV">month,2025,2026
Jan,40,52
Feb,55,61
Mar,32,58
Apr,70,66
May,61,79</textarea>
  <div class="row">
    <button type="submit">Draw</button>
    <span class="spacer"></span>
    <button type="button" data-kind="line" aria-pressed="true">Line</button>
    <button type="button" data-kind="bar" aria-pressed="false">Bar</button>
    <button type="button" data-kind="area" aria-pressed="false">Area</button>
  </div>
</form>
<div class="row" id="series"></div>
<div id="msg"></div>
<div id="chart"></div>

<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-is@18.3.1/umd/react-is.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/recharts@3.10.1/umd/Recharts.js"></script>
<script>
  const h = React.createElement;
  const R = Recharts;
  const COLORS = ['#2563eb', '#d97706', '#0f766e', '#9333ea'];
  const root = ReactDOM.createRoot(document.getElementById('chart'));
  let kind = 'line';
  let state = { rows: [], keys: [], hidden: {} };

  // turn "label,a,b" lines into [{label, a, b}]; the first line names the series
  function parse(text) {
    const lines = text.trim().split('\n').map(l => l.split(',').map(s => s.trim()));
    const head = lines.shift();
    const keys = head.slice(1);
    const rows = [];
    let skipped = 0;
    for (const cells of lines) {
      const row = { label: cells[0] };
      let good = keys.length > 0;
      keys.forEach((k, i) => {
        const n = Number(cells[i + 1]);
        if (cells[i + 1] === '' || cells[i + 1] === undefined || Number.isNaN(n)) good = false;
        row[k] = n;
      });
      good ? rows.push(row) : skipped++;
    }
    return { rows, keys, skipped };
  }

  function draw() {
    const { rows, keys, hidden } = state;
    const Chart = { line: R.LineChart, bar: R.BarChart, area: R.AreaChart }[kind];
    const Mark = { line: R.Line, bar: R.Bar, area: R.Area }[kind];
    const marks = keys.filter(k => !hidden[k]).map(k => {
      const color = COLORS[keys.indexOf(k) % COLORS.length];
      const p = { key: k, dataKey: k, name: k };
      if (kind === 'line') Object.assign(p, { stroke: color, strokeWidth: 2.5 });
      if (kind === 'bar') Object.assign(p, { fill: color });
      if (kind === 'area') Object.assign(p, { stroke: color, fill: color, fillOpacity: 0.2 });
      return h(Mark, p);
    });
    root.render(
      h(R.ResponsiveContainer, { width: '100%', height: '100%' },
        h(Chart, { data: rows, margin: { top: 8, right: 8, left: -16, bottom: 0 } },
          h(R.CartesianGrid, { strokeDasharray: '3 3', vertical: false }),
          h(R.XAxis, { dataKey: 'label' }), h(R.YAxis), h(R.Tooltip), h(R.Legend),
          marks))
    );
  }

  function drawSeriesBoxes() {
    const box = document.getElementById('series');
    box.innerHTML = '';
    state.keys.forEach((k, i) => {
      const lab = document.createElement('label');
      const cb = document.createElement('input');
      cb.type = 'checkbox'; cb.checked = !state.hidden[k];
      cb.addEventListener('change', () => { state.hidden[k] = !cb.checked; draw(); });
      const sw = document.createElement('span');
      sw.style.cssText = 'width:10px;height:10px;border-radius:2px;background:' + COLORS[i % COLORS.length];
      lab.append(cb, sw, document.createTextNode(k));
      box.append(lab);
    });
  }

  function apply() {
    const out = parse(document.getElementById('csv').value);
    state = { rows: out.rows, keys: out.keys, hidden: {} };
    document.getElementById('msg').textContent = out.rows.length === 0
      ? 'No usable rows. Use: label,number,number per line.'
      : (out.skipped ? out.skipped + ' row(s) skipped: every value must be a number.' : '');
    drawSeriesBoxes();
    draw();
  }

  // the form is handled inside the page; nothing is sent anywhere
  document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); apply(); });
  document.querySelectorAll('[data-kind]').forEach(btn => btn.addEventListener('click', () => {
    kind = btn.dataset.kind;
    document.querySelectorAll('[data-kind]').forEach(b => b.setAttribute('aria-pressed', String(b === btn)));
    draw();
  }));
  apply();
</script>
</body>
</html>
Edit the numbers and press Draw. The buttons switch the chart type and the boxes hide a series.
  • Parse once: a parse function turns each line into an object and converts the text to numbers with Number().
  • One root: the page creates the React root once and calls render again with new data.
  • Series boxes: the page draws one Line, Bar or Area per column that is ticked.

When it does not work

What you see Cause Fix
Recharts is not defined The Recharts file ran before React, or did not load Put the four tags in order, Recharts last
reading 'forwardRef' React was not on the page yet when Recharts ran Move the React tag above Recharts
reading 'ForwardRef' The react-is tag is missing Add react-is before Recharts
React is not defined A React 19 address returns 404, it has no UMD file Use React 18.3.1
Unexpected token '<' JSX inside a plain script Use createElement or type="text/babel"
Empty box, no error The chart's parent has no height Give the parent a height

A chart you can hover is more useful than a screenshot, and an .html attachment may open as plain code on a phone. Opening an HTML file on a phone covers why.

Send the working version instead. Paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The scripts from jsDelivr load and run, so the people you send it to can hover the line and edit the numbers themselves. If you change the code later, the same link shows the new version.

For charts drawn without any library, see Line chart in HTML with plain SVG. If your whole app is built with a bundler, Host a built front end as static files covers that route.

Questions people ask

Can I use Recharts without npm or a bundler?

Yes. Recharts publishes a UMD build, which is a script that puts everything on window.Recharts. Load React, ReactDOM and react-is first, then the Recharts file, and call the components from your own script.

Why does the chart area stay empty with no error?

ResponsiveContainer sizes itself from its parent. If the parent has no height, the chart gets zero pixels to draw in. Give the parent a height, such as 240px, or set a fixed width and height on the chart itself.

Why do these examples use React 18 and not React 19?

React stopped publishing UMD builds with version 19, and a plain script tag needs one. The examples pin React 18.3.1, which still has a UMD file. With a build step you can use any React version that Recharts supports.

Do I have to write JSX?

No. JSX is not JavaScript, so a plain script cannot contain it. Call React.createElement directly, as the examples do, or load Babel in the page and mark the script type="text/babel".

Is Recharts free to use?

Recharts is released under the MIT licence, which allows free use. The licence text is in the project repository.

Keep reading