{"id":"4y3cekgxl71rt0b","title":"HTML Forms Without Framework Magic: What Still Works in 2026","slug":"html-forms-without-framework-magic","summary":"Native forms with server actions and honest validation beat heavy client kits on most marketing sites. I wire them without losing UX.","imageUrl":"https://briancrabtree.me/images/journal-html-forms-without-framework-magic.webp","category":"HTML","date":"2026-03-02T18:00:00.000Z","featured":false,"likes":29,"author":"Brian Crabtree","content":"<h2>The platform caught up</h2>\n\n<p>React 19 useActionState, progressive enhancement, and improved browser validation mean the default form element is viable again. I still see teams import 40 KB form libraries for three fields because the starter template did.</p>\n\n<p>Native inputs support autocomplete, inputmode, pattern, required, and aria-invalid without wrappers. Mobile keyboards behave correctly when types are honest.</p>\n\n<p>input type=email with proper validationMessage on custom submit handlers still needs server regex for plus addressing and IDN domains.</p>\n\n<pre><code>&lt;form action=\"/api/contact\" method=\"post\"&gt;\n  &lt;label for=\"email\"&gt;Email&lt;/label&gt;\n  &lt;input id=\"email\" name=\"email\" type=\"email\" autocomplete=\"email\" required&gt;\n  &lt;button type=\"submit\"&gt;Send&lt;/button&gt;\n&lt;/form&gt;</code></pre>\n\n<h2>Labels, errors, and focus</h2>\n\n<p>Associate labels with for and id. Describe errors with aria-describedby pointing to visible text, not title attributes. On submit failure, move focus to the summary region so screen reader users hear what broke.</p>\n\n<p>Disable submit while pending, but show progress text. Do not rely on color alone for errors; text plus icon if brand allows.</p>\n\n<p>datalist for suggestions beats heavy autocomplete libraries for country pickers when lists are static.</p>\n\n<h2>Server validation is the source of truth</h2>\n\n<p>Client validation is convenience; server validation is law. I duplicate rules in schema validators on the server and generate messages the client can mirror. Never trust browser bypass.</p>\n\n<p>For SPAs without SSR, return field errors as JSON and map to inputs. For MPAs, re-render the form with inline messages. Both beat alert() dialogs.</p>\n\n<p>formaction on buttons can target different endpoints in one form for approve versus reject flows without JavaScript routing.</p>\n\n<h2>Security basics people skip</h2>\n\n<p>CSRF tokens on session cookies, honeypots or turnstile for public contact forms, rate limits per IP, and sanitization before storage. File uploads need type and size caps server-side.</p>\n\n<p>Do not put secrets in front-end env vars. Email via API route with keys on server only.</p>\n\n<p>CSRF on JSON APIs uses double-submit cookie or custom header pattern; HTML forms use hidden token field. I do not mix patterns on one endpoint.</p>\n\n<h2>Multi-step without a SaaS tax</h2>\n\n<p>Fieldset and legend group steps. Save draft in session or localStorage for long applications. You do not need a wizard component library if URLs mark steps and back button works.</p>\n\n<p>Payment flows stay with Stripe Elements or hosted checkout; do not roll crypto in a contact form plugin.</p>\n\n<p>File drag-drop UX can be progressive enhancement on input type=file without replacing the native control.</p>\n\n<h2>When I still reach for JS</h2>\n\n<p>Async company lookup, tag chips, and drag-drop uploads need script. I isolate them as islands while the form skeleton stays HTML. Form actions post JSON or FormData depending on backend appetite.</p>\n\n<p>Rate limiting contact forms at edge plus honeypot catches most spam without captcha friction that kills conversion.</p>\n\n<p>When I audit html stacks, When I still reach for JS is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point.</p>\n\n<h2>Contact pattern on this site</h2>\n\n<p>Brief forms should load fast and fail loudly in logs, quietly for users. I implement the smallest stack that meets spam resistance and CRM handoff. Send your fields list; I will sketch native markup before recommending a library.</p>\n\n<p>I log validation failures aggregated, not per field with PII, when debugging production form issues.</p>\n\n<p>When I audit html stacks, Contact pattern on this site is where hours disappear if nobody owns the outcome. I write before-and-after notes, attach PSI or waterfall screenshots, and leave the team a three-item follow-up list. Reproducible wins beat toolchain debates in sprint review. If this section matches your bottleneck, send the URL and whether LCP, INP, CLS, or crawl coverage is the pain point. For a related angle I keep coming back to, see <a href=\"/journal/form-spam-without-captcha-friction/\">Form Spam Without CAPTCHA Friction</a>.</p>","tags":["forms","html","accessibility"],"views":81}