Skip to content

Free browser tools that respect your files — everything runs locally, nothing is uploaded.

HTML Form Builder

Build a working form in the browser and export one self-contained HTML file you own — no account, no server, no external requests.

Fields

Form structure

No fields yet — click a field type above to add it, or load the sample contact form.

Field settings

Select a field in the list to edit its label, validation, and options.

Form settings

Only used in the exported file: it POSTs the form data to this URL with fetch. No action URL means the form simply shows the success message.

Accent color

Live preview

Validates exactly like the exported file — same checks, same messages, first invalid field focused on submit.

Nothing to preview yet — the preview renders your fields as the export will.

Export

One self-contained file: inline CSS, inline JavaScript, system fonts, zero external requests. Your work is saved automatically in this browser (pwt.form-builder.v1).

View HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contact form</title>
<style>
html,body{margin:0;padding:0;}
body{background:#f7f7f4;color:#1c1c1a;}
.pwt-scope{--a:#0e7568;--bg:#f7f7f4;--card:#ffffff;--ink:#1c1c1a;--muted:#5f5f59;--bd:#e3e3dc;--bd2:#cfcfc6;--inp:#ffffff;--err:#b3261e;--ok:#157347;--r-ctl:8px;--r-panel:16px;background:var(--bg);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:16px;line-height:1.6;color-scheme:light;}
.pwt-scope *,.pwt-scope *::before,.pwt-scope *::after{box-sizing:border-box;}
.pwt-wrap{max-width:560px;margin:0 auto;padding:28px 20px 48px;}
.pwt-title{font-size:24px;line-height:1.25;font-weight:700;letter-spacing:-0.01em;margin:0 0 16px;}
.pwt-form{background:var(--card);border:1px solid var(--bd);border-radius:var(--r-panel);padding:24px 22px;}
.pwt-field{margin:0 0 18px;}
.pwt-label{display:block;font-size:14px;font-weight:600;line-height:1.5;margin:0 0 6px;}
.pwt-star{color:var(--err);}
.pwt-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
.pwt-input{display:block;width:100%;min-height:44px;padding:10px 12px;font:inherit;font-size:16px;color:var(--ink);background:var(--inp);border:1px solid var(--bd2);border-radius:var(--r-ctl);}
.pwt-textarea{min-height:110px;resize:vertical;}
.pwt-input:focus{outline:2px solid var(--a);outline-offset:1px;}
.pwt-input::placeholder{color:var(--muted);opacity:1;}
.pwt-invalid{border-color:var(--err) !important;}
.pwt-hint{font-size:14px;color:var(--muted);margin:6px 0 0;}
.pwt-err{font-size:14px;font-weight:500;color:var(--err);margin:6px 0 0;}
.pwt-group{border:0;padding:0;margin:0 0 18px;min-width:0;}
.pwt-options{display:flex;flex-direction:column;gap:2px;}
.pwt-option{display:flex;align-items:center;gap:10px;min-height:44px;font-size:16px;cursor:pointer;}
.pwt-option input{width:18px;height:18px;margin:0;flex:none;accent-color:var(--a);}
.pwt-option input:focus-visible{outline:2px solid var(--a);outline-offset:2px;}
.pwt-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:4px;}
.pwt-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:10px 22px;font:inherit;font-size:16px;font-weight:600;border-radius:var(--r-ctl);cursor:pointer;}
.pwt-btn:focus-visible{outline:2px solid var(--a);outline-offset:2px;}
.pwt-btn-primary{background:var(--a);color:#ffffff;border:1px solid transparent;}
.pwt-btn-primary:hover{filter:brightness(1.08);}
.pwt-btn-primary:disabled{opacity:.6;cursor:default;}
.pwt-btn-reset{background:transparent;color:var(--ink);border:1px solid var(--bd2);}
.pwt-fetch-err{font-size:14px;font-weight:500;color:var(--err);margin:14px 0 0;}
.pwt-success{background:var(--card);border:1px solid var(--ok);border-radius:var(--r-panel);padding:24px 22px;}
.pwt-success-text{font-size:16px;margin:0;}
.pwt-note{font-size:14px;color:var(--muted);margin:8px 0 0;}
.pwt-heading{font-size:19px;font-weight:700;line-height:1.3;margin:6px 0 12px;}
.pwt-para{font-size:15px;color:var(--muted);margin:0 0 16px;}
.pwt-divider{border:0;border-top:1px solid var(--bd);margin:20px 0;}
.pwt-scope [hidden]{display:none !important;}
</style>
</head>
<body>
<div class="pwt-scope">
  <div class="pwt-wrap">
    <h1 class="pwt-title">Contact form</h1>
    <form id="pwt-form" class="pwt-form" method="post" novalidate>
      <div class="pwt-actions">
        <button type="submit" class="pwt-btn pwt-btn-primary">Submit</button>
      </div>
      <p class="pwt-fetch-err" id="pwt-form-error" role="alert" hidden></p>
    </form>
    <div class="pwt-success" id="pwt-success" role="status" tabindex="-1" hidden>
      <p class="pwt-success-text"></p>
    </div>
  </div>
</div>
<script>
(function () {
  'use strict';
  var FIELDS = [];
  var SUCCESS = "Thanks — your response has been recorded.";
  var ACTION = "";
  var validate = function H(a,b){var c=a.type,d=a.label&&a.label.trim()||"This field",e=b&&"string"==typeof b.text?b.text.trim():"",f=!!(b&&b.checked),g=b&&Array.isArray(b.selected)?b.selected:[];if("heading"===c||"paragraph"===c||"divider"===c)return null;if("radio"===c||"checkbox-group"===c)return a.required&&0===g.length?"Select at least one option for "+d+".":null;if("checkbox"===c||"consent"===c)return a.required&&!f?'Check "'+d+'" to continue.':null;if(a.required&&""===e)return d+" is required.";if(""===e)return null;if("email"===c){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))return"Enter a valid email address for "+d+"."}else if("number"===c){var h=Number(e);if(!isFinite(h))return"Enter a number for "+d+".";var i=Number(a.min),j=Number(a.max);if(""!==a.min&&isFinite(i)&&h<i)return d+" must be "+a.min+" or more.";if(""!==a.max&&isFinite(j)&&h>j)return d+" must be "+a.max+" or less."}if("phone"===a.pattern){if(!/^\+?[0-9][0-9 .()\-]{5,19}$/.test(e))return"Enter a valid phone number for "+d+"."}else if("url"===a.pattern){if(!/^https?:\/\/\S+$/i.test(e))return"Enter a valid URL for "+d+" (starting with http:// or https://)."}else if("postal"===a.pattern){if(!/^[0-9]{5}(-[0-9]{4})?$/.test(e))return"Enter a valid postal code for "+d+"."}else if(a.pattern)try{if(!RegExp("^(?:"+a.pattern+")$").test(e))return d+" does not match the expected format."}catch(a){}return null};
  var form = document.getElementById('pwt-form');
  var successBox = document.getElementById('pwt-success');
  var successText = successBox.querySelector('.pwt-success-text');
  var formError = document.getElementById('pwt-form-error');
  var submitBtn = form.querySelector('button[type="submit"]');

  function inputFor(f) { return document.getElementById(f.id); }
  function groupInputs(f) { return form.querySelectorAll('input[name="' + f.id + '"]'); }

  function readValue(f) {
    if (f.type === 'radio' || f.type === 'checkbox-group') {
      var checked = form.querySelectorAll('input[name="' + f.id + '"]:checked');
      var selected = [];
      for (var i = 0; i < checked.length; i++) selected.push(checked[i].value);
      return { text: '', checked: false, selected: selected };
    }
    var el = inputFor(f);
    if (!el) return { text: '', checked: false, selected: [] };
    if (f.type === 'checkbox' || f.type === 'consent') {
      return { text: '', checked: !!el.checked, selected: [] };
    }
    return { text: el.value || '', checked: false, selected: [] };
  }

  function setError(f, msg) {
    var errEl = document.getElementById(f.id + '-err');
    var el = inputFor(f);
    var first = null;
    if (el && el.tagName !== 'FIELDSET') {
      first = el;
    } else {
      first = groupInputs(f)[0] || null;
    }
    if (errEl) errEl.textContent = msg || '';
    if (first) {
      if (msg) first.setAttribute('aria-invalid', 'true');
      else first.removeAttribute('aria-invalid');
    }
    if (el) {
      if (msg) el.classList.add('pwt-invalid');
      else el.classList.remove('pwt-invalid');
    }
  }

  function validateAll() {
    var firstBad = null;
    for (var i = 0; i < FIELDS.length; i++) {
      var f = FIELDS[i];
      var msg = validate(f, readValue(f));
      setError(f, msg);
      if (msg && !firstBad) firstBad = f;
    }
    return firstBad;
  }

  function showSuccess() {
    form.hidden = true;
    successText.textContent = SUCCESS;
    successBox.hidden = false;
    successBox.focus();
  }

  form.addEventListener('submit', function (e) {
    e.preventDefault();
    formError.hidden = true;
    formError.textContent = '';
    var bad = validateAll();
    if (bad) {
      var target = bad.type === 'radio' || bad.type === 'checkbox-group'
        ? groupInputs(bad)[0]
        : inputFor(bad);
      if (target) target.focus();
      return;
    }
    if (ACTION) {
      submitBtn.disabled = true;
      var oldLabel = submitBtn.textContent;
      submitBtn.textContent = 'Sending…';
      fetch(ACTION, { method: 'POST', body: new FormData(form) })
        .then(function (res) {
          if (res.ok) {
            showSuccess();
          } else {
            formError.textContent = 'The endpoint responded with error ' + res.status + '. Check the action URL and try again.';
            formError.hidden = false;
          }
        })
        .catch(function () {
          formError.textContent = 'Could not reach the endpoint. Check the URL, its CORS settings, and your connection, then try again.';
          formError.hidden = false;
        })
        .then(function () {
          submitBtn.disabled = false;
          submitBtn.textContent = oldLabel;
        });
    } else {
      showSuccess();
    }
  });

  form.addEventListener('reset', function () {
    for (var i = 0; i < FIELDS.length; i++) setError(FIELDS[i], '');
    formError.hidden = true;
    formError.textContent = '';
  });

  function clearErrorFor(target) {
    if (!target || !target.name) return;
    for (var i = 0; i < FIELDS.length; i++) {
      if (FIELDS[i].id === target.name) {
        setError(FIELDS[i], '');
        break;
      }
    }
  }
  form.addEventListener('input', function (e) { clearErrorFor(e.target); });
  form.addEventListener('change', function (e) { clearErrorFor(e.target); });
})();
</script>
</body>
</html>

Quick Answer

An HTML form builder generates the markup, styling, and validation code for a web form so you do not hand-write it. This free builder assembles fields in your browser, shows a live preview that validates exactly like the final file, and exports one self-contained .html file with inline CSS and JavaScript — no CDN, no fonts, no account. Point it at your own endpoint with an action URL or let it show a success message; the file is yours to host anywhere.

How It Works

1

Add fields from the palette — text, email, dropdowns, groups, consent, and layout blocks — then reorder, duplicate, or delete them in the structure list

2

Configure validation per field: label, required, placeholder, help text, options, number min and max, and pattern checks (phone, URL, postal code, or your own regex)

3

Style the form: name it, set the submit text and success message, pick one of eight accents, light or dark theme, rounded corners, and an optional reset button

4

Test the live preview — it runs the same validation script as the export, shows the same messages, and focuses the first invalid field on submit

5

Export with Copy HTML or Download form.html — one self-contained file with inline CSS and JavaScript and zero external requests

Key Facts

  • Fourteen field types: text, email, number, phone, long text, dropdown, radio and checkbox groups, single checkbox, date, consent, heading, paragraph, and divider
  • Validation runs identically in the preview and the export — required, email format, number min and max, patterns, consent, and group minimums, with the same messages
  • Preset pattern checks for phone, URL, and US postal code, plus custom regular expressions
  • One self-contained .html export: inline CSS, inline JavaScript, system font stack, zero external requests
  • Optional action URL POSTs form data to your own endpoint with fetch, with success and error handling built in
  • Eight accent colors and a light or dark theme baked into the export with explicit colors
  • Accessible output: labels bound to inputs, fieldset and legend for groups, errors wired via aria-describedby, first invalid field focused on submit
  • Work is saved automatically in this browser (localStorage); copy the HTML or download form.html at any time

Frequently Asked Questions

How does validation work in the exported form?

The exported file uses a semantic form with the novalidate attribute, so the browser never blocks submit with its own bubbles. An inline script at the end of the file re-runs the same checks the live preview runs — required fields, email format, number minimum and maximum, pattern mismatches, required consent boxes, and a minimum of one selection for required groups — and shows the exact same messages. Errors are attached with aria-describedby and aria-invalid, and the first invalid field receives focus on submit. The script is embedded from the very function the preview uses, so the two cannot drift apart.

Is this form builder free?

Yes — free and unlimited, with no account, no watermarks, and no caps. Hosted form services charge for scale: Tally’s free tier is generous but team features sit behind Tally Pro at 34/month. This builder exports a plain HTML file you host yourself, so there is no per-response cost at all.

Where do submitted responses go?

Two modes. Without an action URL the form simply validates and shows your success message — nothing is sent anywhere. With an action URL, the exported file POSTs the FormData to that endpoint using fetch when the visitor submits, and shows a success or error message based on the response. The endpoint is yours: your own server, a serverless function, or any handler that accepts form-data. Note the endpoint must allow cross-origin requests (CORS) for the browser to read the response.

Is my data private?

Yes. The builder runs entirely in your browser — field definitions are saved to localStorage and never uploaded, and the HTML export is generated locally. The exported file itself makes zero external requests: styling and the validation script are inline and the typeface is the device’s system font stack, so a finished form works offline and loads nothing from any CDN.

Can I host the exported form anywhere?

Yes. The export is a single .html file with no dependencies, so it works on any static host (GitHub Pages, Netlify, S3, a Raspberry Pi), in an iframe, attached to an email, or opened straight from disk. It is responsive with a 560px maximum content width, uses explicit colors in both light and dark themes so host pages cannot wash it out, and sizes inputs at 44px for touch screens.

Why use this instead of embedding a form service?

A hosted embed loads third-party scripts, caps responses, and keeps your form behind their dashboard. This export contains no vendor script tags, has no submission limits, and is plain HTML you can edit in any text editor. The trade-off is honest: without an action URL the form records nothing by itself — wire it to your own endpoint if you need to collect responses.