All posts

HTML vs JSX: every difference that breaks a paste

Why class becomes className, how style, booleans, whitespace, braces, entities, forms, SVG and event handlers change, and what React 19 warns about.

By Last reviewed 7 min readReactJSXHTMLSVG

JSX looks like HTML, so pasting a template or an SVG icon into a component feels like it should work. It rarely does. JSX is JavaScript: each tag becomes a function call and each attribute a key in an object. Some differences stop the build, some only print a warning in the console, and a few change the page without telling you.

To convert as you read, paste markup into the HTML to JSX Converter. It applies every rule below and lists what it removed or changed, with the line and column.

The quick reference

HTMLJSXWhy
class="card"className="card"class is a reserved word; React uses the DOM property name
for="email"htmlFor="email"Same reason: the DOM property is htmlFor
tabindex="0"tabIndex={0}Attributes use React's camelCase names
stroke-width="2"strokeWidth="2"A dash cannot appear in a camelCase prop name
style="margin-top: 4px"style={{ marginTop: "4px" }}style takes an object
<br><br />Every tag must be closed
<input value="x"><input defaultValue="x" />value makes the field controlled
<option selected><select defaultValue="…">React does not support selected on options
onclick="save()"onClick={save}Handlers are functions, not strings
<!-- note -->{/* note */}JSX has no HTML comments
Use {name}Use {"{"}name{"}"}Braces start an expression
Two root elements<>…</>An expression returns one element

Attribute names: className, htmlFor and camelCase

The React docs explain the naming in Writing Markup with JSX: attributes become keys of JavaScript objects, so "in React, many HTML and SVG attributes are written in camelCase", and "since class is a reserved word, in React you write className instead, named after the corresponding DOM property". for becomes htmlFor for the same reason. React 19 still renders class if you leave it, but logs Invalid DOM property `class`. Did you mean `className`?

The names follow the DOM: readOnly, maxLength, autoComplete, srcSet, httpEquiv. React keeps a list of about 500, and warns in development when you use the HTML spelling of one.

The exceptions

Three kinds of attribute keep their HTML names. The docs say "for historical reasons, aria-* and data-* attributes are written as in HTML with dashes". Custom elements, any tag with a dash such as <my-widget>, receive attributes as written (see custom HTML elements). And an attribute React does not know is passed through as long as it is lowercase.

SVG

SVG presentation attributes lose their dashes: stroke-width, fill-rule, clip-path and stroke-linecap become strokeWidth, fillRule, clipPath and strokeLinecap. Namespaced names become camelCase too: xlink:href is xlinkHref, xml:space is xmlSpace and xmlns:xlink is xmlnsXlink. viewBox is already camelCase. The inkscape: and sodipodi: attributes that drawing apps add have no React name, so remove them.

Values: styles, booleans and numbers

In JSX, style takes an object. The common components reference says property names "need to be written as camelCase", and that a number such as width: 100 gets px "unless it's a unitless property", such as opacity or zIndex. Vendor prefixes start with a capital, except ms: -webkit-transition becomes WebkitTransition and -ms-transform becomes msTransform. A custom property stays a quoted key, "--accent". Passing a string throws an error, so the component does not render at all.

Two CSS habits do not survive the move. React sets each property on the element's style separately, so !important is ignored. An object holds one value per key, so a fallback such as two background declarations keeps only the last one.

A boolean attribute in HTML is on when it is present, whatever its value: disabled="false" still disables the button. In JSX, a bare disabled means true, and disabled={false} leaves the attribute out. Numeric props such as tabIndex, colSpan and maxLength are typed as numbers in React's TypeScript types, so write tabIndex={0}.

Tags: closing, one root and comments

"JSX requires tags to be explicitly closed": <img>, <br> and <input> become <img />, <br /> and <input />, and an empty <div></div> may be written <div />. A component returns one element, so two siblings need a parent; a fragment, <>…</>, groups them "without leaving any trace in the browser HTML tree". HTML comments become JavaScript comments inside braces, {/* … */}.

Text: braces, entities and whitespace

A { in text starts a JavaScript expression, and TypeScript rejects a bare } or > with Unexpected token. Did you mean `{'}'}` or `&rbrace;`? Write braces as strings, {"{"} and {"}"}, and angle brackets as &lt; and &gt;.

JSX understands the HTML 4 named entities, such as &nbsp;, &copy; and &mdash;, but not the newer HTML5 names: &check; shows as written, and &lang; becomes a different bracket than it does in a browser. HTML also accepts &copy without its semicolon; JSX does not. Numeric references such as &#169; work, but SWC, the compiler in Next.js, fails on any &# that is not a complete reference, so write a literal &# as &amp;#.

Whitespace follows its own rules. JSX trims each line of text and drops whitespace that contains a line break between tags. That breaks a space you can see on the page: in Read the <a>guide</a> split over two lines, the space before the link disappears. Write it as {" "} at the end of the line. Spaces next to block elements are not shown by browsers anyway. Text in <pre> keeps every space and line break only as a template literal: <pre>{`npm run build`}</pre>.

Forms: value versus defaultValue

This one compiles and then misbehaves. In React, value makes a field controlled, and the <input> reference warns: "If you pass value without onChange, it will be impossible to type into the input." React logs You provided a `value` prop to a form field without an `onChange` handler. For a starting value the user can change, use defaultValue, and defaultChecked for checkboxes and radio buttons.

Selects work differently from HTML too. The <select> reference says "passing a selected attribute to <option> is not supported": put the option's value in <select defaultValue>, as an array when the select has multiple. A <textarea> takes its text from defaultValue as well, not from its children.

Scripts, raw HTML and event handlers

An HTML handler such as onclick="save()" is a string of code. It runs in the global scope, with its own this and an implicit event. A React handler is a function you pass: onClick={save}, as Responding to Events shows. No tool can turn the string into a function safely, so move the code into your component. React ignores a string handler and warns that it expected a function.

Text inside a JSX element is escaped, so code in a <script> must go into dangerouslySetInnerHTML, which the docs say "should be used with extreme caution" because of XSS. It still may not run. A script React inserts while rendering in the browser is created through innerHTML, and browsers do not run scripts made that way. Only <script async src="…"> is loaded by React 19, which moves it to the head and de-duplicates it. Load other code with an import, in useEffect, or with your framework's script component. A JSON-LD block is data, so dangerouslySetInnerHTML is the right fit.

A worked example

A form field with a selected option, a style, braces and a handler:

<div class="field">
  <label for="plan">Plan</label>
  <select id="plan" name="plan">
    <option value="free">Free</option>
    <option value="pro" selected>Pro</option>
  </select>
  <p class="hint" style="margin-top: 4px; font-size: 13px">Billed {monthly} &mdash; cancel any time.</p>
  <button type="submit" onclick="save()" disabled>Save</button>
</div>

The HTML to JSX Converter turns it into:

<div className="field">
  <label htmlFor="plan">Plan</label>{" "}
  <select id="plan" name="plan" defaultValue="pro">
    <option value="free">Free</option>
    <option value="pro">Pro</option>
  </select>
  <p className="hint" style={{ marginTop: "4px", fontSize: "13px" }}>
    Billed {"{"}monthly{"}"} &mdash; cancel any time.
  </p>
  <button type="submit" /* onclick="save()" */ disabled>Save</button>
</div>

The {" "} after the label keeps the gap a browser shows between it and the select, since both are inline. The selected option became the select's defaultValue. The handler stays as a comment, and the Warnings list says to write onClick={…} instead.

Before you paste

Run minified markup through the HTML Formatter first: it indents it and shows the unclosed tags a browser would repair. The HTML Entity Encoder tells you what an entity stands for. After converting, read every warning: removed handlers and inline scripts are yours to rewrite.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.