send.webforms.toRead the docs →

Convert an HTML form to React, Vue, Svelte or JSON Schema

Paste a plain HTML form and get a working component back, with a submit handler that posts in the background and shows a result instead of reloading the page. Or get the JSON Schema of what the form collects, for validating it on the other end. It runs in your browser.

ContactForm.tsx4 fields

"use client";

import { useState } from "react";

type Status = "idle" | "sending" | "sent" | "error";

export function ContactForm() {
  const [status, setStatus] = useState<Status>("idle");

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    // Read the form before awaiting: React clears currentTarget once the
    // handler yields.
    const form = event.currentTarget;
    setStatus("sending");
    try {
      const res = await fetch(form.action, {
        method: "POST",
        body: new FormData(form),
        headers: { Accept: "application/json" },
      });
      if (!res.ok) throw new Error(String(res.status));
      form.reset();
      setStatus("sent");
    } catch {
      setStatus("error");
    }
  }

  return (
    <form action="https://send.webforms.to/wft_your_key" method="POST" className="contact" onSubmit={onSubmit}>
      <p hidden><input name="_gotcha" tabIndex={-1} autoComplete="off" /></p>

      <label htmlFor="name">Name</label>
      <input id="name" name="name" type="text" required maxLength={80} />

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />

      <label htmlFor="topic">What is it about?</label>
      <select id="topic" name="topic" defaultValue="support">
        <option value="sales">Sales</option>
        <option value="support">Support</option>
      </select>

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required></textarea>

      <button type="submit">Send</button>
      {status === "sent" ? <p role="status">Thanks, your message was sent.</p> : null}
      {status === "error" ? <p role="alert">Something went wrong. Please try again.</p> : null}
    </form>
  );
}

Worth knowing

  • A selected option became defaultValue on its select, which is how React sets an initial choice.

What each conversion takes care of

React is the one that fights pasted HTML hardest. class becomes className, for becomes htmlFor, void elements are closed, inline styles become objects, and numeric props like maxLength are passed as numbers so the file compiles under TypeScript. Initial values move to defaultValue and defaultChecked, and a selected option moves up to its select, because React warns about the plain HTML versions on an uncontrolled field. Curly braces in text are escaped so they stay text.

Vue and Svelte take the markup nearly as written, so the work is the submit handler: @submit.prevent in Vue, and Svelte 5's onsubmit attribute with runes for state. Svelte evaluates curly braces in text, so those are turned into entities.

All three read the form before the first await. Browsers clear currentTarget once an event handler yields, and forgetting that is the most common reason a fetch-based form works in development and throws in production.

JSON Schema follows the field types: email and URL inputs get formats, numbers get their min and max, selects and radio groups become enums, and required fields are listed. Hidden fields that start with an underscore are left out, because they are form settings rather than data.

Where the submission goes

Every component posts to the form's own action URL and asks for JSON back, which is how a form endpoint reports success or a specific error. The framework guides show the same pattern in context: React, Next.js, Vue and Svelte. Need the form itself first? Build one with the form generator.