HTML to JSX
Paste an HTML fragment of up to 100 KB, press Convert, and read JSX beside it. Attributes take React's spelling (class becomes className, for becomes htmlFor, tabindex="0" becomes tabIndex={0}), style attributes become objects with custom properties quoted, void tags close with />, and braces, angle brackets, ampersands and quotes in text are escaped so the result compiles. The difference is the report under the output: the changes are counted by kind, everything the converter would not carry across is listed, and what needs your attention is flagged, from scripts, event handler attributes, javascript: URLs and iframe srcdoc documents to attribute names JSX cannot hold and tags that were never closed. The markup is read as characters, never rendered or run in this page, so nothing in it is fetched. With Wrap in component on, the default, the JSX comes back as an export default function under the name you choose, ready for Copy JSX or Download .jsx.
Your HTML
Ctrl+Enter, or Cmd+Enter on a Mac, converts from the box.
JSX, 10 lines, Snippet.jsx
export default function Snippet() {
return (
<>
<label htmlFor="email" className="field">Email {"{"}required{"}"}</label>{" "}
<input type="email" id="email" placeholder="you@example.com" />
<div style={{ color: "red", "--gap": "4px" }}>We'll "never" share it</div>
{/* script removed */}
</>
);
}
The sample is already converted. Paste your own HTML and press Convert.
- Attributes renamed
- 2
- Void tags closed
- 1
- Style objects
- 1
- Flagged
- 1: script
Change report
Flagged for your attention:
- 1 script element removed without running. Each is marked {/* script removed */} where it stood; rebuild that behavior in React code.
Changes made:
- 2 attributes renamed to React's spelling: for → htmlFor, class → className.
- 1 void element closed with />: input.
- 1 style attribute turned into an object: property names in camelCase, custom properties quoted, every value kept as a string.
- 5 characters in text escaped for JSX: braces as {"{"} and {"}"}, angle brackets, ampersands and quotes as entities, invisible characters as \u escapes.
- 1 space between inline elements written as {" "}, because a JSX line break would otherwise drop it.
- Several top-level nodes are wrapped in a fragment, <>...</>, because JSX returns one root.
Worked cases
<label htmlFor="qty" className="field">Qty</label><input type="number" id="qty" min="1" /><div style={{ "--gap": "4px", marginTop: "2px" }}>Row</div><p>Use {"{"}qty{"}"} for "each"</p>
Up to 100 KB of HTML at a time, nested up to 256 levels deep, converted in this tab with nothing uploaded. The wrap switch and component name are kept in this browser; the HTML never is, and Start over above the tool forgets the settings.
Common questions
- What does the converter change to make HTML valid JSX?
- Attribute names take React's spelling: class becomes className, for becomes htmlFor, and names such as tabindex, readonly, maxlength, colspan and crossorigin become tabIndex, readOnly, maxLength, colSpan and crossOrigin, with SVG ones like stroke-width and xlink:href becoming strokeWidth and xlinkHref. Hyphenated names become camelCase only where React renames them: accept-charset and http-equiv on any element, and on SVG elements the presentation attributes React lists, such as stroke-width, clip-path and fill-opacity. Any other hyphenated name, data-* and aria-* included, is kept exactly as written. Void elements such as input, br and img close with />. Boolean attributes are written as shorthand, so disabled="" becomes disabled, and a whole-number value on a prop React types as a number, such as tabindex="0" or colspan="2", becomes {0} or {2}. HTML comments become {/* */} comments. On form fields, value on a text input becomes defaultValue, checked becomes defaultChecked, the text of a textarea moves to defaultValue and a selected option becomes defaultValue on its select, so the field stays editable without an onChange handler. Every one of these is counted in the change report.
- How are inline styles converted?
- style="margin-top: 2px; --gap: 4px" becomes style={{ marginTop: "2px", "--gap": "4px" }}. Property names become camelCase, vendor prefixes follow React (-webkit-transition becomes WebkitTransition, -ms-transform becomes msTransform), and custom properties keep their exact name in quotes. Every value stays a string exactly as written, so no unit is added or lost, and a semicolon inside quotes or url() stays part of its value. A property written twice keeps its last value, placed after the others, as the cascade reads it. !important is removed and flagged, because a React style object cannot set priority; CSS comments and declarations that cannot be read are dropped and listed. In a .tsx file, React's TypeScript types do not list custom properties, so a style object holding one needs a cast such as as React.CSSProperties.
- What happens to scripts and onclick handlers?
- They are removed and flagged, never run. A script element becomes the comment {/* script removed */} where it stood, and none of its code is copied into the JSX. An attribute such as onclick="save()" is dropped, because its value is code in a string and React needs a function; the report names them so you can attach onClick yourself. Any attribute whose name begins with on goes the same way in any mix of capitals, hyphens and underscores, so on-click, ON_CLICK and _onclick are removed too. A javascript: or vbscript: URL is removed wherever it could load or navigate: href on any element, SVG and MathML included, xlink:href, src, action, formaction, poster, data and similar attributes, the content of a meta refresh, and the to, from, values and by of an SVG animation that sets a link or an event handler. That holds when the scheme is disguised with capitals, leading spaces, tabs, line breaks or character references, and when the attribute name is written with capitals, hyphens or underscores, such as xlink-href. An iframe srcdoc is a whole page that can carry its own scripts, so it is removed and flagged too. A data: URL can be one as well, because Safari's engine shows a data: URL whose type it cannot display as a page and runs its scripts, and older versions cannot display some image and video types at all. So data: URLs in frames, objects, embeds and SVG use are removed unless they are plain text: that covers the src or data of an iframe, frame, object or embed, the href of an SVG use, and a data: URL an SVG animation sets on an href through to, from, values or by. One stays only when its media type, read after one round of percent-decoding, is empty (data:,) or text/plain, and the only parameters it may carry are ;base64 and ;charset= with a name such as utf-8; a % still left after that decoding removes it too. Every other type is removed and flagged, images, PDFs, audio, video, image/svg+xml, application/octet-stream and made-up types included, so serve those from their own URL. An img, audio, video, source or SVG image keeps its data: URL, because an image or media element never runs a script from its source. In a style attribute, a declaration holding a javascript: URL or an expression(), or a behavior, -ms-behavior or -moz-binding property, is removed and listed, even when CSS escapes or comments hide it.
- What happens to HTML from a source I do not trust?
- It is never rendered. The markup is read as plain characters by the converter's own tokenizer in this tab: it is not handed to the browser's HTML parser, not inserted into the page and not fetched, so nothing in it runs or loads while you convert, and the JSX comes back as text. What the converter takes out is a fixed removal list, each item named in the change report: script elements, event handler attributes, javascript: and vbscript: URLs, srcdoc documents, data: URLs in frames, objects, embeds and SVG use unless they are plain text, script in inline styles, and the props React reserves, such as dangerouslySetInnerHTML, key and ref. Every name on that list is matched in any mix of capitals, hyphens and underscores, so on-click and dangerously-set-inner-html are removed too, and xlink-href loses a javascript: URL just as xlink:href does. Everything outside that list is copied across as written, such as an ordinary link, an image URL, a form that posts elsewhere or a style element's CSS, so review the JSX before you ship markup from a source you do not trust. A base element is kept and flagged, because it changes how every relative URL on the page resolves.
- How are whitespace, braces and quotes in text handled?
- Text is escaped so it reads back exactly: { and } become {"{"} and {"}"}, and <, >, &, " and ' become <, >, &, " and '. A non-breaking space becomes , and invisible characters such as zero-width spaces and bidirectional controls become \u escapes, so nothing hidden lands in your source. Runs of spaces and line breaks collapse to one space, as a browser displays them. A line break just inside a tag or beside a block element is dropped, the way JSX drops it, and a space between two inline elements on separate lines is kept as {" "}. Inside pre, textarea, style elements and anything styled white-space: pre, whitespace is kept exactly, written as a string or template literal.
- Do I get a React component or just JSX?
- Either. With Wrap in component on, the default, the output is export default function Snippet() { return (...); } under the name you type, which must start with a capital letter and hold only letters, digits and underscores, up to 64 characters. Several top-level elements are wrapped in a fragment, <>...</>. Turn the switch off to get the bare JSX. Copy JSX puts it on the clipboard and Download .jsx saves it as Name.jsx, or snippet.jsx when nothing is wrapped. If the browser refuses the clipboard, the JSX appears in a read-only box for you to copy by hand.
- What about SVG, tables and HTML that is not well formed?
- The tree follows the browser's parsing rules for the cases a converter meets most. SVG keeps its case-sensitive names, such as viewBox, linearGradient and foreignObject. A row placed directly in a table gets the tbody a browser adds, which React's nesting check expects, and that is listed as a change. A paragraph or list item left open closes where the next one starts, as HTML allows. An end tag with nothing to close is ignored, an element whose required end tag is missing is closed where a browser closes it, and a tag name JSX cannot use, such as Word's o:p, is unwrapped with its text kept; each of those is flagged. Formatting tags closed in the wrong order and text sitting directly inside a table are flagged too, rather than rearranged the way a browser would.
- How much HTML can it convert, and is anything saved?
- Up to 100 KB of UTF-8, 102,400 bytes, per conversion, nested up to 256 levels deep. Past either limit the page says which one was reached and converts nothing, rather than cutting the HTML short. The JSX itself is capped at 2,000,000 characters, a size only very deep nesting reaches. Your HTML is never stored. The wrap switch and component name are remembered in this browser once you change them, and Start over above the tool forgets them.
Text conversion only; does not execute HTML or infer React behavior/event handlers. Text and attribute values keep their characters, runs of whitespace collapse as a browser displays them except where the element preserves them (pre, textarea, style, white-space: pre), and anything removed is listed in the report.