Use Babel in a single HTML file

Babel is a JavaScript compiler. Load @babel/standalone into a page and a script tag marked text/babel is compiled in the browser, so JSX and new syntax work with no build step.

Babel is a JavaScript compiler: it reads new syntax and writes older JavaScript. For a single HTML file, that means @babel/standalone, a build of Babel for browsers. Put your code in a <script type="text/babel"> block and Babel compiles it as the page opens.

Try it first. Type a name and press the button. The code in the block uses a private field, a class field, an arrow function and ??, and Babel rewrites all of it.

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>Babel in one HTML file</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card { max-width: 420px; background: #fff; border-radius: 12px; padding: 16px 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  input { width: 100%; box-sizing: border-box; padding: 8px; font-size: 16px; border: 1px solid #cfd4dc; border-radius: 8px; }
  button { margin-top: 10px; padding: 8px 14px; font-size: 15px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  #out { margin: 12px 0 0; font-size: 18px; font-weight: 600; }
  #status { margin: 8px 0 0; font-size: 13px; color: #6b7280; }
  pre { margin: 12px 0 0; padding: 10px; max-height: 120px; overflow: auto; background: #0f172a; color: #e2e8f0; font-size: 12px; line-height: 1.45; border-radius: 8px; white-space: pre-wrap; word-break: break-all; }
</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.29.9/babel.min.js"></script>
</head>
<body>
<div class="card">
  <label for="name">Your name</label>
  <input id="name" placeholder="leave empty for guest">
  <button id="add" type="button">Add one</button>
  <p id="out">Hello, guest! Count: 0</p>
  <p id="status">Compiling...</p>
  <pre id="js" aria-label="JavaScript after Babel"></pre>
</div>

<!-- The browser skips this block. Babel finds it, compiles it, and runs the result. -->
<script type="text/babel" data-presets="env" id="source">
  class Counter {
    #count = 0;                              // private field
    add = (n = 1) => (this.#count += n);     // class field with an arrow function
    get total() { return this.#count; }
  }
  const counter = new Counter();
  const nameBox = document.getElementById('name');
  const out = document.getElementById('out');

  const show = () => {
    const name = nameBox.value.trim() || null;
    out.textContent = `Hello, ${name ?? 'guest'}! Count: ${counter.total}`;
  };
  document.getElementById('add').addEventListener('click', () => { counter.add(); show(); });
  nameBox.addEventListener('input', show);
</script>

<script>
  // Show what Babel produced from the block above (same preset).
  const src = document.getElementById('source').textContent;
  const code = Babel.transform(src, { presets: ['env'] }).code;
  document.getElementById('js').textContent = code;
  document.getElementById('status').textContent = 'Babel ' + Babel.version + ' compiled the block. Output below:';
</script>
</body>
</html>
A text/babel block compiled by Babel 7.29.9 from jsDelivr. The dark box shows the JavaScript Babel produced from the block.

That is what "Babel in HTML" means here: writing JSX or modern syntax in one file, with no webpack, no npm and no build folder.

What happens when the page loads

A browser does not run a <script> whose type it does not recognise. MDN says the content is treated as a data block and is not processed. That is why text/babel is safe to write in a page.

The browser skips the text/babel block, Babel rewrites it into plain JavaScript, and the result runs.
The browser skips the text/babel block, Babel rewrites it into plain JavaScript, and the result runs.

Babel finds each block, reads its text and puts the compiled result into a new script element. We measured the order: Babel runs after the page has been read, so a normal script placed after the block still runs first.

When Babel loads, the console prints a warning: "You are using the in-browser Babel transformer. Be sure to precompile your scripts for production". That is expected.

The smallest working page

Four steps give a working page:

  1. Load Babel. One script tag in the <head>, with the version in the address.
  2. Write a text/babel block. Add a data-presets attribute to say what to compile.
  3. Load what the code uses first. JSX, for example, needs React.
  4. Open the page and read the console. Babel reports syntax errors there.
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.29.9/babel.min.js"></script>

<script type="text/babel" data-presets="env">
  const greet = (name = 'guest') => `Hello, ${name}!`;
  document.body.append(greet());
</script>

The Babel docs show the same tag with unpkg. jsDelivr serves the same file at a versioned address, and we loaded version 7.29.9 from it.

Presets decide what gets rewritten

A preset is a ready-made bundle of plugins. The Babel docs describe @babel/preset-env as the one that includes the transforms needed for modern JavaScript. In a page you name presets in data-presets, separated by commas.

Two presets matter in a single file. env rewrites modern syntax and react rewrites JSX. Using both is written data-presets="env,react".

Preset env rewrites an arrow function and a template string. Preset react rewrites JSX into a React.createElement call.
Preset env rewrites an arrow function and a template string. Preset react rewrites JSX into a React.createElement call.

Try your own code in the playground. Pick a sample, change the preset, and read what Babel returns. The Typo sample shows how an error looks: Babel stops and nothing runs.

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>Babel playground</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
  button { padding: 7px 12px; font-size: 14px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; cursor: pointer; }
  button.on { background: #2563eb; color: #fff; border-color: #2563eb; }
  #go { background: #16a34a; color: #fff; border-color: #16a34a; }
  label { font-size: 13px; color: #4b5563; display: block; margin: 8px 0 4px; }
  textarea, pre { box-sizing: border-box; width: 100%; margin: 0; padding: 10px; font: 13px/1.45 ui-monospace, Consolas, monospace; border-radius: 8px; }
  textarea { height: 110px; border: 1px solid #cfd4dc; resize: vertical; }
  pre { height: 190px; overflow: auto; background: #0f172a; color: #e2e8f0; white-space: pre-wrap; word-break: break-all; }
  pre.err { background: #fff7ed; color: #9a3412; border: 1px solid #fdba74; }
  #info { font-size: 13px; color: #6b7280; margin: 6px 0 0; }
</style>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.29.9/babel.min.js"></script>
</head>
<body>
<div class="row">
  <span style="font-size:13px">Sample:</span>
  <button type="button" data-sample="modern">Modern JS</button>
  <button type="button" data-sample="jsx">JSX</button>
  <button type="button" data-sample="bad">Typo</button>
</div>
<label for="src">Your code</label>
<textarea id="src" spellcheck="false"></textarea>
<div class="row" style="margin-top:8px">
  <span style="font-size:13px">Preset:</span>
  <button type="button" data-preset="env" class="on">env</button>
  <button type="button" data-preset="react">react</button>
  <button type="button" id="go">Compile</button>
</div>
<label for="js">What the browser would receive</label>
<pre id="js" tabindex="0"></pre>
<p id="info"></p>

<script>
  const samples = {
    modern: 'const total = (a, b = 2) => `Total: ${a + b}`;\nconst user = { name: null };\nconsole.log(user?.name ?? "guest", total(5));',
    jsx: 'const Card = ({ title }) => (\n  <div className="card">\n    <h1>{title}</h1>\n  </div>\n);',
    bad: 'const el = ;\nconst total = (a, b => a + b;'
  };
  const src = document.getElementById('src');
  const js = document.getElementById('js');
  const info = document.getElementById('info');
  let preset = 'env';

  function compile() {
    try {
      js.className = '';
      js.textContent = Babel.transform(src.value, { presets: [preset] }).code;
      info.textContent = 'Babel ' + Babel.version + ' with preset "' + preset + '". Compiled without errors.';
    } catch (err) {
      js.className = 'err';
      js.textContent = err.message;   // a syntax error stops the compile and gives a message
      info.textContent = 'Babel ' + Babel.version + ' reported an error. Nothing would run.';
    }
  }

  document.querySelectorAll('[data-sample]').forEach((b) => b.addEventListener('click', () => {
    const k = b.dataset.sample;
    src.value = samples[k];
    preset = k === 'jsx' ? 'react' : 'env';
    document.querySelectorAll('[data-preset]').forEach((p) => p.classList.toggle('on', p.dataset.preset === preset));
    compile();
  }));
  document.querySelectorAll('[data-preset]').forEach((b) => b.addEventListener('click', () => {
    preset = b.dataset.preset;
    document.querySelectorAll('[data-preset]').forEach((p) => p.classList.toggle('on', p === b));
    compile();
  }));
  document.getElementById('go').addEventListener('click', compile);

  src.value = samples.modern;
  compile();
</script>
</body>
</html>
Babel.transform in a page. The dark box is the compiled result; an orange box means Babel found an error.

The same compiler is also a function. It takes a string and returns a string, which is how the playground works:

const output = Babel.transform(input, { presets: ['env'] }).code;

The Babel docs also note that config files do not work in @babel/standalone, because there is no file system.

JSX needs React in scope

A browser cannot run JSX. In a normal script tag, the first <b> tag throws a SyntaxError. We measured Chromium's message as Unexpected token '<'.

With data-presets="react", Babel compiles JSX to React.createElement calls. The preset-react docs say this classic style needs React in scope. We measured it: with only Babel on the page, the block fails with "React is not defined".

Pin React 18.3.1 for a single HTML file. React 19 no longer produces UMD builds. When we asked jsDelivr for the UMD file of react 19.0.0 it returned a 404, and the React team recommends an ESM-based CDN for 19.

A finished example: a quick poll

This page loads React 18.3.1, ReactDOM and Babel, then writes a small component in JSX. Votes are stored in React state, and the form is handled in the page.

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>Quick poll with React and Babel</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card { max-width: 420px; background: #fff; border-radius: 12px; padding: 16px 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  h1 { font-size: 18px; margin: 0 0 10px; }
  .opt { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
  .opt button { flex: 1; text-align: left; padding: 9px 12px; font-size: 15px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; cursor: pointer; position: relative; overflow: hidden; }
  .bar { position: absolute; left: 0; top: 0; bottom: 0; background: #dbeafe; z-index: 0; }
  .opt span.t { position: relative; z-index: 1; }
  .n { min-width: 28px; text-align: right; font-weight: 600; }
  form { display: flex; gap: 8px; margin-top: 12px; }
  input { flex: 1; min-width: 0; padding: 8px; font-size: 16px; border: 1px solid #cfd4dc; border-radius: 8px; }
  form button { padding: 8px 14px; font-size: 15px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  .note { font-size: 13px; color: #6b7280; margin: 10px 0 0; }
</style>
<!-- React 18 still ships browser-ready builds; Babel turns the JSX below into plain JavaScript -->
<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/@babel/standalone@7.29.9/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel" data-presets="react">
  const { useState } = React;

  function Poll() {
    const [options, setOptions] = useState([
      { label: 'Tabs', votes: 2 },
      { label: 'Spaces', votes: 3 },
    ]);
    const [text, setText] = useState('');
    const total = options.reduce((sum, o) => sum + o.votes, 0);

    const vote = (i) =>
      setOptions(options.map((o, j) => (j === i ? { ...o, votes: o.votes + 1 } : o)));

    const add = (e) => {
      e.preventDefault();                       // handled in the page, nothing is sent
      const label = text.trim();
      if (!label) return;
      setOptions([...options, { label, votes: 0 }]);
      setText('');
    };

    return (
      <div className="card">
        <h1>Which do you prefer?</h1>
        {options.map((o, i) => (
          <div className="opt" key={o.label}>
            <button type="button" onClick={() => vote(i)}>
              <div className="bar" style={{ width: total ? (o.votes / total) * 100 + '%' : 0 }} />
              <span className="t">{o.label}</span>
            </button>
            <span className="n">{o.votes}</span>
          </div>
        ))}
        <form onSubmit={add}>
          <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Add an option" aria-label="New option" />
          <button type="submit">Add</button>
        </form>
        <p className="note" id="total">{total} votes in total</p>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<Poll />);
</script>
</body>
</html>
Click an option to vote. Add your own option with the form. The JSX is compiled by Babel in the page.
  • Load order: React and ReactDOM come before Babel, and the text/babel block comes last.
  • State: useState holds the options and the text box.
  • The form: the submit handler calls preventDefault(), so nothing is sent anywhere.

Compile in the page, or before shipping

The cost of this approach is the compiler. We downloaded babel.min.js 7.29.9 and it measured 3.1 MB. Every visitor downloads it before your code runs.

Compiling in the page keeps one file. Compiling before shipping leaves only plain JavaScript for the visitor.
Compiling in the page keeps one file. Compiling before shipping leaves only plain JavaScript for the visitor.

The Babel docs say that if you use Babel in production, you should normally not use @babel/standalone. They compile ahead of time instead:

npm install --save-dev @babel/core @babel/cli @babel/preset-env
npx babel src --out-dir lib

The usual end state is the same single file: paste the compiled JavaScript into a normal script tag and remove Babel. Hosting a built front end covers what to do with a full build.

When it does not work

What you see Cause Fix
Nothing happens and there is no error The Babel script did not load, so the block stays a data block Check the address and the version in the script tag
Unexpected token '<' JSX is inside a normal script tag Use type="text/babel" and data-presets="react"
React is not defined The react preset needs React, and it is not on the page Load React and ReactDOM above the block
A 404 for the React file React 19 has no UMD build Load react@18.3.1 and react-dom@18.3.1
A function from the block is undefined in a normal script Babel runs the block after plain scripts Keep the code in one text/babel block
Babel reports an error about experimental syntax jsx Babel.transform was called with only env Add react to the presets
A console warning about the in-browser transformer Normal for @babel/standalone Ignore it, or compile ahead of time
The page is slow to start Babel is 3.1 MB and runs first Paste the compiled JavaScript instead

If the page stays blank, HTML JavaScript not working goes through the causes in order. Loading a CSS library from a CDN the same way is covered in adding Tailwind to an HTML file.

A Babel page is easier to show working than to describe. An .html attachment may open as plain text on a phone. Opening an HTML file on a phone explains why.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written, its scripts run, and Babel and React load from jsDelivr, so anyone with the link can try it without an account. If you change the code later, the same link shows the new version.

Questions people ask

What does type="text/babel" do?

It stops the browser from running the block. A script type the browser does not recognise is treated as a data block. Babel, once loaded, finds each text/babel block, compiles its text and runs the result.

Why do I get "React is not defined" with JSX?

The react preset turns JSX into React.createElement calls, so React has to be loaded before the block runs. Add the React and ReactDOM script tags above the Babel block. We measured this error with only Babel on the page.

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

React 19 no longer produces UMD builds, the kind of file a plain script tag can load. React 18.3.1 still has them on jsDelivr, so a single HTML file can load it with no build step. React recommends an ESM-based CDN for version 19.

Should I use @babel/standalone on a public website?

The Babel docs say that if you use Babel in production you should normally not use @babel/standalone. Every visitor downloads the compiler and waits for it to run. For a prototype, a demo or an internal tool it is a quick way to start. For a public site, compile ahead of time.

Do I need Babel at all for modern JavaScript?

Only for JSX, which a browser cannot run directly, and for syntax your target browsers cannot read. Plain JavaScript with no JSX can go in a normal script tag, and then Babel is not needed.

Keep reading