splitforms.com
← Back to the journal

HTMX Contact Form Without a Backend (2026)

Build a working HTMX contact form with hx-post, inline success and error states, and zero backend code — submissions land in a spam-filtered dashboard.

Start free — 500 submissions/moSee pricing →No credit card. Paid plans from $5/mo.

Why htmx is a natural fit for a hosted form endpoint

htmx enhances ordinary HTML elements with attributes that trigger AJAX requests, but it doesn't change what gets sent over the wire. A form with hx-post submits the same request body a plain <form> would send with the hx- attributes removed — application/x-www-form-urlencoded by default, the same content type browsers have posted since HTML forms existed. That matters for a hosted form endpoint: splitforms' https://splitforms.com/api/submit already expects exactly that, whether or not JavaScript is involved.

Because htmx doesn't invent a new request format, upgrading a plain HTML form to htmx is purely additive: add hx-post and a couple of related attributes, keep the same field names, the same access_key, the same honeypot field, and the backend never needs to know or care that htmx is involved. That's the whole idea behind a hosted HTML form backend — it's built to accept a POST from anywhere, a plain browser, a hand-rolled fetch() call, or htmx, without special-casing any of them.

The 60-second baseline: a form that works before htmx loads

Progressive enhancement means the form should work before you write a single hx-attribute. Get this version working first — it's a complete, submittable contact form with zero JavaScript:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="hidden" name="redirect" value="https://yoursite.com/thanks" />

  <label for="name">Name</label>
  <input id="name" name="name" type="text" required />

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <!-- Honeypot spam trap -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />

  <button type="submit">Send message</button>
</form>

action and method point straight at splitforms; access_key identifies which of your forms the submission belongs to; the hidden redirect field sends the visitor to a thank-you page after a native POST. Skip it and the browser renders the raw JSON response instead of redirecting — the submission still goes through, it just looks broken. The checkbox is the honeypot: real visitors never see or fill it, so any submission with botcheck checked gets dropped on the server. Want this exact pattern without htmx at all? The copy-paste HTML contact form code is the same setup.

Upgrading to htmx: hx-post and hx-swap

Load htmx from a CDN, then add hx-post and hx-swap="none" to the same form — keep the plain action, method, and redirect field from the baseline, for reasons explained below:

<script src="https://unpkg.com/htmx.org@2.0.4"></script>

<form
  action="https://splitforms.com/api/submit"
  method="POST"
  hx-post="https://splitforms.com/api/submit"
  hx-swap="none"
>
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="hidden" name="redirect" value="https://yoursite.com/thanks" />

  <label for="name">Name</label>
  <input id="name" name="name" type="text" required />

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <!-- Honeypot spam trap -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />

  <button type="submit">Send message</button>
</form>

Version pinned above as of mid-2026 — check htmx.org for the current release before you ship.

hx-post tells htmx to intercept the submit event and send the request in the background instead of navigating the page. hx-swap="none" tells htmx not to inject the response into the page at all, which needs some explaining: htmx's usual pattern pairs hx-target with hx-swap to drop a server-rendered HTML fragment straight into the DOM. splitforms' endpoint doesn't return an HTML fragment — it returns JSON, the same response contract the fetch()-based AJAX version of this form relies on. Let the default swap run against that response and a visitor would see a raw {"success":true} blob on the screen instead of a message. hx-swap="none" skips the injection and leaves the UI to you, which is the next section.

Notice the form still carries its plain action, method, and the hidden redirect field from the baseline — that's not leftover cruft. If the htmx script is blocked, fails to load, or the CDN it's served from has a bad minute, the browser falls back to a normal POST, and the redirect field is what keeps that fallback from dumping JSON on the page instead of showing a thank-you message.

Inline success and error states

Because hx-swap="none" skips the automatic injection, the "thanks" and error messages have to come from somewhere else: htmx's own request-lifecycle events. Listen for the completed request with hx-on::after-request, check event.detail.successful, and toggle two elements you've already written into the page:

<form
  action="https://splitforms.com/api/submit"
  method="POST"
  hx-post="https://splitforms.com/api/submit"
  hx-swap="none"
  hx-on::after-request="
    if (event.detail.successful) {
      this.reset();
      this.hidden = true;
      document.getElementById('form-success').hidden = false;
    } else {
      document.getElementById('form-error').hidden = false;
    }
  "
>
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input name="name" type="text" required />
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>

  <!-- Honeypot spam trap -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />

  <button type="submit">Send message</button>
</form>

<p id="form-success" role="status" aria-live="polite" hidden>
  Thanks — your message is on its way.
</p>
<p id="form-error" role="alert" aria-live="assertive" hidden>
  Something went wrong. Please try again or email us directly.
</p>

That's the entire client-side logic for a working success and error state — one attribute, no fetch(), no async/await, no separate script file to maintain. role="status" and aria-live="polite" on the success message (and role="alert" with aria-live="assertive" on the error one) make sure screen readers announce the result even though nothing on the page navigated. If you want a loading indicator while the request is in flight, add hx-indicator pointing at an element — htmx toggles a visibility class on it automatically for the duration of the request.

Spam protection that still works

None of this changes what splitforms does with the submission once it lands. Every form gets the same default server-side filtering regardless of how the POST arrived — plain browser submit, htmx, or a hand-rolled fetch() call: a honeypot field, a time-trap that flags submissions completed faster than a human plausibly could, and IP-based rate limiting. The botcheck checkbox in every example above is that honeypot — bots that fill in every field they find still fill it in, real visitors never see it, and routing the request through htmx doesn't change any of that. There's nothing extra to configure specifically for htmx; see the spam protection page for the full default stack, included free on every plan.

Gotchas to watch for

Three things that catch people out the first time:

  • hx-boost on an ancestor. If a layout wraps the page in hx-boost="true" for whole-site navigation, it boosts descendant forms and links too, not just <a> tags. A form with its own hx-post takes priority on that element, but a plain fallback form nested somewhere inside a boosted container can start behaving like an unplanned SPA navigation. Set hx-boost="false" on the form itself if that happens.
  • File uploads need explicit encoding. htmx doesn't automatically switch a form to multipart encoding just because it contains a file input. Add hx-encoding="multipart/form-data" (or set enctype="multipart/form-data" directly on the <form>) or the file data won't reach the endpoint.
  • Don't wire real secrets into hx- attributes. The access_key in every example on this page is a public identifier meant to sit in client-side markup — that's how splitforms and every hosted form endpoint works, and it isn't a secret. But hx-post, hx-headers, and hx-vals are just HTML attributes: anything you put in them ships in your page source. Don't point them at some other API that expects a real bearer token or private key.

Next steps

  • Prefer writing the request yourself? The AJAX contact form guide covers the same success and error handling written as plain fetch().
  • Framework-agnostic reference: the HTML contact form setup guide covers the plain-HTML version in more depth.
  • Full endpoint reference and every optional field: docs.

FAQ

What is an htmx contact form?

A contact form where the submit button is wired with hx-post instead of relying only on the form's native action attribute. htmx intercepts the submit event, sends the same POST request a plain form would send, and lets you update the page from the response — no full reload, no separate JavaScript framework.

Do I need a backend to use htmx for a contact form?

No. htmx just changes how the request is sent, not where it goes. Point hx-post at a hosted endpoint like https://splitforms.com/api/submit the same way you'd point a plain form's action attribute, and there's no server code to write or host.

How does hx-post work on a form?

hx-post tells htmx to send the form's fields as a POST request to the given URL using AJAX instead of letting the browser navigate. It reads the same fields a native submit would, so field names, hidden inputs, and honeypots all carry over unchanged.

How do I show a success message after an htmx form submission?

If your endpoint returns an HTML fragment, a plain hx-target/hx-swap pair will drop it into the page. If it returns JSON — as splitforms does — set hx-swap="none" and use hx-on::after-request to check event.detail.successful and reveal a success or error element yourself.

Does an htmx contact form still work if JavaScript fails to load?

Only if you keep the form's native action and method attributes alongside the hx- ones. htmx enhances an existing form rather than replacing it, so if the script fails to load or is blocked, the browser falls back to a normal POST to the same endpoint.

Can I upload files with an htmx form?

Yes, but add hx-encoding="multipart/form-data" (or enctype="multipart/form-data" on the form) explicitly. htmx doesn't switch encoding automatically just because a file input is present.

Does a honeypot still stop spam on a form submitted through htmx?

Yes — the honeypot is just a hidden form field, and htmx submits the same fields a native form would. Nothing about routing the request asynchronously changes how splitforms' server-side honeypot, time-trap, or rate limiting evaluate the submission.

Ready to wire this up? Get a free splitforms access key — 500 submissions a month, honeypot and time-trap spam filtering, and email notifications on every plan, including free.

Related articles

More practical guidance from tutorials.

Browse the journal →
Tutorials

FormData in JavaScript: Submit Forms with fetch (2026 Guide)

The complete FormData guide: reading form fields, appending files, sending multipart and url

9 min readRead →
Tutorials

Custom Auto-Responder Emails for HTML Forms (HTML + CSS, 2026)

Design fully branded auto-responder emails for your HTML forms: paste your own HTML/CSS temp

9 min readRead →
Tutorials

Send Form Emails From Your Own Domain (Custom SMTP, 2026)

Send form notification and auto-responder emails from your own domain with custom SMTP: encr

10 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewWhat Is a Form Backend? (Complete Guide)Start here →GuideSelf-Hosted vs SaaS Form BackendRead →GuideForm Backend vs Form BuilderRead →GuideWhere Do Form Submissions Go?Read →ReferenceForm backend guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 submissions a month for free.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe