HTML to JSX Converter

Paste HTML and get JSX you can drop into a React component. class becomes className, inline styles become objects, SVG attributes get camelCased, and curly braces in your text are escaped so JSX does not read them as expressions. Notes under the output list anything the converter could not fix for you, such as inline onclick handlers.

HTML
JSX

1 HTML to JSX Converter example

HTML

<label for="q" class="sr-only">Search</label>
<input id="q" type="search" autofocus
  style="font-size: 14px; -webkit-appearance: none">
<svg viewBox="0 0 20 20">
  <path fill-rule="evenodd" d="M8 4l6 6-6 6"/>
</svg>

JSX, with Component on

export default function SearchBox() {
  return (
    <>
      <label htmlFor="q" className="sr-only">Search</label>
      <input
        id="q"
        type="search"
        autoFocus={true}
        style={{ fontSize: "14px", WebkitAppearance: "none" }}
      />
      <svg viewBox="0 0 20 20"><path fillRule="evenodd" d="M8 4l6 6-6 6" /></svg>
    </>
  );
}

2 What changes, and why

Attribute names

JSX compiles to JavaScript, where class and for are reserved words, so they become className and htmlFor. Multi-word attributes follow the DOM property names: tabIndex, readOnly, maxLength, colSpan, httpEquiv. data-* and aria-* stay as they are.

Inline styles

The style prop takes an object. Properties are camelCased, -webkit-x becomes WebkitX and -ms-x becomes msX (lowercase, a React quirk). Values stay strings, except unitless ones like zIndex and opacity, because React appends px to other bare numbers. !important is dropped, since React cannot set it.

Text

A brace in text would start an expression, so it becomes {'{'}. Comments become {/* */}. Whitespace that HTML would render between inline elements is kept as {" "}, because JSX throws away whitespace that contains a line break.

Form fields

value and checked on inputs become defaultValue and defaultChecked, a selected option moves to defaultValue on the select, and textarea content moves into defaultValue. Otherwise React warns about a controlled field with no onChange.

3 Inline handlers still need manual work

Inline handlers like onclick="save(this)" are wrapped in an arrow function so the output compiles, but this no longer points at the element and the function probably does not exist in your component. Treat every converted handler as a to-do. Framework syntax such as Vue's @click or :class is not valid JSX and is skipped, with a note under the output.

Your browser's own parser reads the HTML into an inert template element, so nothing in it runs or loads. The parser also repairs the markup the way a browser does: an unclosed <p> is closed, and a bare <tr> inside a table gains the <tbody> the browser would add. The JSX therefore matches the DOM the page was rendering, which can differ from the source you pasted.

How to use the HTML to JSX Converter tool

  1. Paste HTML into the left panel, or click Upload HTML to load an .html or .svg file.
  2. The JSX appears on the right as you type. There is no convert button.
  3. Leave Wrap roots in <> ticked if you pasted more than one top-level element. JSX needs a single parent.
  4. Tick Component to get a full export default function, and type its name in the box next to it. Names must start with a capital letter.
  5. Check the notes under the output for anything that needs a human, such as inline onclick handlers or skipped framework attributes, then click Copy JSX.

How to convert HTML to JSX in JavaScript and Python

JavaScript (browser)

const RENAME = { class: "className", for: "htmlFor", tabindex: "tabIndex", readonly: "readOnly", maxlength: "maxLength" };
const VOID = new Set(["br", "hr", "img", "input", "link", "meta", "source", "wbr"]);

const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());

function styleObject(css) {
  const pairs = css.split(";").filter((d) => d.includes(":")).map((d) => {
    const [prop, ...rest] = d.split(":");
    return `${camel(prop.trim().replace(/^-ms-/, "ms-"))}: ${JSON.stringify(rest.join(":").trim())}`;
  });
  return `{{ ${pairs.join(", ")} }}`;
}

function toJsx(node, depth = 0) {
  const pad = "  ".repeat(depth);
  if (node.nodeType === 3) {
    const text = node.nodeValue.trim().replace(/[{}]/g, (c) => `{'${c}'}`);
    return text ? pad + text : "";
  }
  if (node.nodeType !== 1) return "";
  const tag = node.localName;
  const attrs = Array.from(node.attributes).map((a) => {
    if (a.name === "style") return `style=${styleObject(a.value)}`;
    return `${RENAME[a.name] || a.name}=${JSON.stringify(a.value)}`;
  });
  const open = `<${tag}${attrs.length ? " " + attrs.join(" ") : ""}`;
  const kids = Array.from(node.childNodes).map((c) => toJsx(c, depth + 1)).filter(Boolean);
  if (!kids.length || VOID.has(tag)) return `${pad}${open} />`;
  return `${pad}${open}>\n${kids.join("\n")}\n${pad}</${tag}>`;
}

const tpl = document.createElement("template");
tpl.innerHTML = '<label for="q" class="sr-only">Search</label><input id="q" style="font-size: 14px">';
console.log(Array.from(tpl.content.childNodes).map((n) => toJsx(n)).join("\n"));

A template element parses without running scripts or loading images, so it is the safe way to walk untrusted markup in a page. This version handles only the common renames. SVG attributes, boolean attributes and whitespace need more work.

Python

import json
import re
from html.parser import HTMLParser

RENAME = {"class": "className", "for": "htmlFor", "tabindex": "tabIndex", "readonly": "readOnly"}
VOID = {"br", "hr", "img", "input", "link", "meta", "source", "wbr"}


def camel(s):
    return re.sub(r"-([a-z])", lambda m: m.group(1).upper(), s)


def style_object(css):
    pairs = []
    for decl in css.split(";"):
        if ":" in decl:
            prop, value = decl.split(":", 1)
            pairs.append(f"{camel(prop.strip())}: {json.dumps(value.strip())}")
    return "{{ " + ", ".join(pairs) + " }}"


class ToJsx(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.out = []

    def attrs(self, attrs):
        parts = []
        for name, value in attrs:
            if name == "style":
                parts.append("style=" + style_object(value or ""))
            elif value is None:
                parts.append(RENAME.get(name, name) + "={true}")
            else:
                parts.append(f"{RENAME.get(name, name)}={json.dumps(value)}")
        return (" " + " ".join(parts)) if parts else ""

    def handle_starttag(self, tag, attrs):
        end = " />" if tag in VOID else ">"
        self.out.append(f"<{tag}{self.attrs(attrs)}{end}")

    def handle_startendtag(self, tag, attrs):
        self.out.append(f"<{tag}{self.attrs(attrs)} />")

    def handle_endtag(self, tag):
        if tag not in VOID:
            self.out.append(f"</{tag}>")

    def handle_data(self, data):
        self.out.append(re.sub(r"[{}]", lambda m: "{'" + m.group(0) + "'}", data))

    def handle_comment(self, data):
        self.out.append("{/*" + data.replace("*/", "* /") + "*/}")


p = ToJsx()
p.feed('<label for="q" class="sr-only">Search {all}</label><input id="q" required style="font-size: 14px">')
print("".join(p.out))

html.parser reports attributes with no value as None, which is how the boolean ones become {true}. It does not fix up broken markup the way a browser does, so run messy HTML through a formatter first.

Common errors and how to fix them

Adjacent JSX elements must be wrapped in an enclosing tag
A component returned two or more top-level elements. Wrap them in a fragment (<>...</>) or a real element. The Wrap roots in <> option does this for you.
Warning: Invalid DOM property `class`. Did you mean `className`?
HTML was pasted into JSX unchanged. React still renders it but logs the warning, and for needs the same fix (htmlFor). Run the markup through the converter to fix every attribute at once.
The `style` prop expects a mapping from style properties to values, not a string
JSX needs style={{ marginTop: "8px" }}, not style="margin-top: 8px". Property names are camelCased and values are strings or numbers.
Expected `onClick` listener to be a function, instead got a value of `string` type
An inline onclick="..." attribute was carried over as a string. React only accepts a function, so write onClick={() => save()} and define save in the component.
You provided a `value` prop to a form field without an `onChange` handler
value makes an input controlled, so React expects you to manage its state. For an initial value only, use defaultValue (and defaultChecked for checkboxes), which is what the converter outputs.

Specifications and references

This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.

Frequently Asked Questions

Why does JSX use className instead of class?

JSX compiles to JavaScript, where class is a reserved word, so React follows the DOM property name className. The same goes for for, which becomes htmlFor.

How are inline styles converted?

The style string becomes an object with camelCased keys: background-color turns into backgroundColor, -webkit-transform into WebkitTransform and -ms-flex into msFlex. Values stay strings except for unitless properties like zIndex and opacity, and !important is dropped because React cannot set it.

Does it convert SVG icons?

Yes. Hyphenated attributes such as stroke-width, fill-rule and clip-path are camelCased, viewBox keeps its capital B, and xlink:href becomes xlinkHref. data- and aria- attributes are left alone.

What happens to onclick and other inline handlers?

They are renamed to onClick, onChange and so on, and wrapped in an arrow function so the output compiles. The code inside is left as it was, so check any use of this and any global function it calls.

Why is there {" "} in the output?

JSX drops whitespace that contains a line break, while HTML renders it as a single space. Where a space between two inline elements would otherwise disappear, the converter writes it out explicitly as {" "}.

Before and after the conversion

Clean up messy markup first with the HTML formatter, shrink an icon before pasting it with the SVG optimizer, or type the props that arrive from an API with JSON to TypeScript. To convert the same markup to Markdown, use HTML to Markdown.

More Data Converters

Browse all data converters

Copied to clipboard!