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.
<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> 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>
</>
);
} 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 {" "}.
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.