splitforms.com
100% client-side · no signup

HTML to JSX Converter with a React form mode

Converting HTML to JSX means renaming a handful of attributes (class to className, for to htmlFor), turning inline styles into objects and closing every tag. Paste HTML below to get JSX, a function component, or a working React/Next.js form component.

Last updated

Try a sample:
Output

Posts to https://splitforms.com/api/submit with your access_key and a botcheck honeypot.

644 chars

ContactForm.tsx

"use client";

import { useState, type FormEvent } from "react";

// Free splitforms endpoint: submissions land in your inbox and dashboard.
const ENDPOINT = "https://splitforms.com/api/submit";

export default function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setStatus("sending");
    try {
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      if (!response.ok) throw new Error(`Request failed: ${response.status}`);
      form.reset();
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

  return (
    <form
      className="contact-form"
      action={ENDPOINT}
      method="POST"
      onSubmit={handleSubmit}
    >
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input type="checkbox" name="botcheck" style={{ display: "none" }} tabIndex={-1} />
      {/* Contact form */}
      <label htmlFor="name">Name</label>
      <input id="name" name="name" type="text" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        autoComplete="email"
        required
      />
      <label htmlFor="message">Message</label>
      <textarea
        id="message"
        name="message"
        rows={5}
        maxLength={2000}
        required
      />
      <label className="consent">
        <input type="checkbox" name="consent" value="yes" defaultChecked />
        {" "}You can reply by email
      </label>
      <button
        type="submit"
        className="btn"
        // TODO: onclick="trackSubmit()" → onClick={() => { … }}
        disabled={status === "sending"}
      >
        Send message
      </button>
      {status === "success" && <p role="status">Thanks! Your message has been sent.</p>}
      {status === "error" && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  );
}

What changed

  • Renamed attributes: class → className (3×), for → htmlFor (3×), autocomplete → autoComplete (2×), maxlength → maxLength
  • HTML comments → {/* */}
  • Void tags such as <input> and <br> self-closed (3×)
  • checked → defaultChecked: React warns when checked has no onChange handler
  • Inline event handlers (onclick="…") left as // TODO comments: React needs a function, not a string
  • Form wired to splitforms: fetch(ENDPOINT, { body: new FormData(form) }) with an access_key field and a botcheck honeypot
  • Replace YOUR_ACCESS_KEY with the access key from your splitforms dashboard

Form mode wires your form to a free splitforms endpoint: no API route, submissions land in your inbox and dashboard.

Get a free access key →

Runs entirely in your browser. Your HTML is parsed in an inert <template>, never executed, and never sent anywhere.

Key facts

What it outputsJSX, a named function component, or a React/Next.js form client component
Where it runsIn your browser. HTML is parsed in an inert <template>: never executed, never uploaded
Attribute renames62 HTML attributes (class, for, tabindex, maxlength…), plus hyphenated and xlink: SVG attributes
Form modeonSubmit + fetch(new FormData(form)) with sending, success and error states
LanguagesJavaScript (.jsx) or TypeScript (.tsx)
PriceFree, no signup
Last updated

What changes when you convert HTML to JSX?

JSX compiles to JavaScript, so attribute names follow JavaScript and DOM-property rules: most become camelCase, inline styles become objects, every tag closes, and handlers are functions. The table lists each change the converter makes, with the react.dev page that documents it.

HTMLJSXWhySource
class="card"className="card"class is a reserved word in JavaScriptreact.dev: common props
for="email"htmlFor="email"React uses the DOM property namereact.dev: common props
style="font-size: 14px"style={{ fontSize: "14px" }}An object with camelCase keys; numbers allowedreact.dev: common props
tabindex="0"tabIndex={0}camelCase, and a numberreact.dev: common props
onclick="send()"onClick={send}Handlers are functions, not stringsreact.dev: common props
data-id="7" aria-label="Close"data-id="7" aria-label="Close"Unchanged: data-* and aria-* keep their dashesreact.dev: common props
maxlength, readonly, autocomplete, autofocusmaxLength, readOnly, autoComplete, autoFocuscamelCase input propsreact.dev: <input>
value="Ada"defaultValue="Ada"Uncontrolled initial valuereact.dev: <input>
checkeddefaultCheckedUncontrolled initial state for checkbox and radioreact.dev: <input>
<textarea>Hi</textarea><textarea defaultValue="Hi" />Children are not allowed in a React textareareact.dev: <textarea>
<option selected><select defaultValue=…>selected on <option> is not supportedreact.dev: <select>
stroke-width="2"strokeWidth="2"SVG attributes are camelCase tooreact.dev: Writing markup with JSX
<br> <img src=… alt=…><br /> <img src=… alt=… />Every tag must be closedreact.dev: Writing markup with JSX

The converter also turns <!-- comments --> into {/* comments */}, wraps several top-level elements in a <>…</> fragment, escapes {, }, < and > in text, and leaves string handlers such as onclick="…" as // TODO comments, because a string can't become a function safely.

How does form mode turn an HTML form into a React component?

Form mode outputs a "use client" component that keeps your markup and adds the submit logic: an onSubmit handler that calls fetch(ENDPOINT, { method: "POST", body: new FormData(form) }), a status state (sending, success, error), a disabled submit button while sending, and a message after the request. Fields stay uncontrolled, so there is no useState per input.

With Send to splitforms ticked, the endpoint becomes https://splitforms.com/api/submit and two fields are added: a hidden access_key input and a hidden botcheck checkbox that bots fill and people never see. Form mode wires your form to a free splitforms endpoint: no API route, submissions land in your inbox and dashboard, with rule-based spam filtering on every submission.

Untick it to keep your form's own action URL. For a full walkthrough, see how to add a contact form to Next.js or how to add a contact form to React.

Why does React warn about value without onChange?

A value prop makes an input controlled, so React keeps it pinned to that value. Without an onChange handler the field can't be edited, and React logs a warning that starts “You provided a `value` prop to a form field without an `onChange` handler” ( react.dev: <input>). Pasted HTML almost always means “start with this value”, so the converter writes defaultValue and defaultChecked instead. Hidden, submit, button, checkbox and radio inputs keep value, since React doesn't warn for them.

Can I convert a table row or an SVG on its own?

Yes. The converter parses your HTML the way a <template> element does, so a lone <tr>, a few <td> cells or a <thead> come out exactly as pasted. Parsing the same snippet as a whole document drops those rows and cells. SVG keeps its case-sensitive attributes such as viewBox, hyphenated attributes become camelCase (stroke-width to strokeWidth), and xlink:href becomes xlinkHref.

Related guides

HTML to JSX FAQ

What is the difference between HTML and JSX?

JSX is a JavaScript syntax extension that looks like HTML but compiles to JavaScript, so it follows JavaScript's rules. Attributes become camelCase props (class becomes className, tabindex becomes tabIndex), inline styles become objects, every tag must be closed, event handlers are functions instead of strings, and a component returns a single root element or a <>…</> fragment.

Why does React use className instead of class?

Because class is a reserved word in JavaScript, and JSX attributes become keys of JavaScript objects. React names most props after DOM properties instead: className for class, htmlFor for for. The exceptions are data-* and aria-* attributes, which keep their dashes.

Why does React warn about a value prop without onChange?

A value prop makes the input controlled: React keeps it equal to that value, so without an onChange handler nobody can type in it. React logs a warning and suggests defaultValue for a field that should stay editable. That is why this converter turns value into defaultValue, and checked into defaultChecked, on editable inputs. Hidden, submit and button inputs keep value because it is not user-editable there.

How do I convert an inline style string to a React style object?

Split the declarations on semicolons, camelCase each property name (font-size becomes fontSize, -webkit-transition becomes WebkitTransition) and put the values in an object: style={{ fontSize: "14px" }}. Numbers can stay numbers on unitless properties such as opacity, zIndex and lineHeight. CSS custom properties keep their name as a quoted key. The converter does all of this, and drops !important because React's style prop can't apply it.

Is my HTML uploaded or executed?

No. The converter runs entirely in your browser. Your HTML is parsed with template.innerHTML on a <template> inside a detached document, which is inert: scripts don't run, images don't load, and nothing is inserted into the page. No network request is made.

What does form mode generate?

When the HTML contains a <form>, form mode outputs a "use client" React component. It renders your fields as uncontrolled inputs, posts them with fetch and new FormData(form), disables the submit button while sending, and shows a success or error message. With "Send to splitforms" ticked, it posts to https://splitforms.com/api/submit and adds a hidden access_key field and a botcheck honeypot, so the form works without writing an API route.

Does the output work in the Next.js App Router?

Yes. The JSX and component output are plain server-compatible markup. The form component starts with "use client" because it uses useState and an onSubmit handler, which only run in client components. Save it as a .tsx (TypeScript on) or .jsx file and import it from any page.

Why does a <tr> or <td> pasted on its own survive here?

Because the converter parses in template context. Parsing a fragment as a full HTML document (as DOMParser's text/html mode does) drops table rows and cells that sit outside a <table>. Template parsing keeps them. When a <tr> sits directly inside <table>, the converter adds the <tbody> that browsers insert, which also keeps React from warning about the nesting.

Is splitforms free?

Yes, to start. The free plan includes 500 submissions in total, one time, with email notifications and rule-based spam filtering. Pro is $5 a month and Annual is $49 a year. The converter itself is free and needs no account.

Converted the form? Give it a backend in one step.

Point the form at splitforms and submissions arrive in your inbox and dashboard, with a honeypot, rule-based spam filtering and webhooks. The free plan includes 500 submissions in total; Pro is $5 a month.

Get a free access key →