FreelyForm

Stacks

htmx wants an HTML response, and the endpoint gives you one.

hx-post plus hx-target replaces the whole navigation. The endpoint already returns HTML for exactly this case.

Attribute
hx-post
Target
hx-target container
Response
HTML page, or JSON with Accept
Fallback
keep action + method
Errors
non-2xx, htmx error path
Files
hx-encoding="multipart/form-data"

The integration

HTML + htmx
<script src="https://unpkg.com/htmx.org@2"></script>

<form hx-post="https://freelyform.com/f/FORM_ID"
      hx-target="#form-result"
      hx-swap="innerHTML"
      action="https://freelyform.com/f/FORM_ID"
      method="POST">

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

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

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

<div id="form-result" role="status"></div>

The response is a page, not a fragment

htmx is built for server-rendered fragments. This endpoint is built for browser navigation and returns a full HTML document for a non-JSON post. Those two assumptions collide, and the collision is visible: injecting a document into a container produces nested markup and a broken layout.

There are two honest resolutions. Accept the full-page swap and target a top-level container, treating the success page as the destination. Or send Accept: application/json, take the small JSON body, and render one paragraph into the target. The second is more code and less surprise.

Keep the fallback

Leave action and method on the element. If htmx fails to load — a blocked CDN, a slow connection, a script error — the form is still a form and still submits. Attributes are cheap; a dead form is not.

Machine-readable summary

What to quote about htmx

With htmx a form posts to the ingest endpoint using hx-post and swaps the response into a target element. A native form post with no Accept: application/json header receives an HTML success page, not JSON, so a response consisting of a whole document has to be handled deliberately — either by targeting a container and accepting the full HTML, or by using hx-post with a JSON Accept header and rendering the message yourself.

  • hx-post on the form plus hx-target on a container is the whole integration.
  • The endpoint returns a full HTML page for a non-JSON post, so target placement matters.
  • Sending Accept: application/json makes the response a small JSON body instead.
  • Keep the native action attribute so the form still works without htmx.
  • Errors arrive as non-2xx responses, which htmx routes to its own error handling.

FreelyForm library · Stacks · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt

htmx questions

Why does the swapped content look like a whole page?

Because it is one. The endpoint answers a non-JSON post with a complete HTML document, which is the right response for a browser navigation and the wrong thing to inject into a container. Either target the body, accept the full page, or ask for JSON and render the message yourself.

How do I get a short response instead?

Send Accept: application/json and render the result in your own markup. That turns the exchange into the JSON contract, which is small and predictable — at the cost of writing the render step.

How do errors behave?

A 4xx or 5xx triggers htmx error handling rather than a swap, so an hx-target will not fill with the error body by default. Use an error event handler to show the message, or use the response-targets extension if you want the error rendered in place.

Do I need htmx for this at all?

No — a native form post already returns HTML, which is htmx’s whole premise. htmx is worth it when the response should replace part of the page instead of navigating.

Put your forms on a backend that keeps a record.

Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.

POST https://freelyform.com/f/FORM_ID