Back to blog
tutorial

The HTML Form Action Attribute: A Complete Guide

What the HTML form action attribute does, what to put in it when you have no server, how formaction works, and a checklist for when your form action won't submit.

J
Jesper Christiansen

You can spend an afternoon getting a form’s markup right — labels, validation, a button that looks the part — and still have it do nothing useful. The attribute that decides whether the data goes anywhere is one word in the opening tag: action.

action is the address on the envelope. The rest of the form fills the envelope; action says where to mail it. Get it wrong and the submission either reloads the same page or vanishes. Get it right and your form is wired to wherever you want the data to land — your own server, a serverless function, or a hosted endpoint that handles the backend for you.

This guide covers what the action attribute actually does, every kind of value you can put in it, the question most tutorials skip — what do you point it at when you don’t run a server? — how to route one form to different URLs with formaction, and a checklist for when a form refuses to submit.

Table of Contents

What the HTML form action attribute does

The action attribute specifies the URL that processes a form submission. When the user clicks submit, the browser collects the form’s fields and sends them to that URL. That’s the whole job.

Here’s the minimal version:

<form action="/subscribe" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required>
  <button type="submit">Subscribe</button>
</form>

When this form is submitted, the browser sends email to /subscribe on the current site. Whatever lives at /subscribe — a server route, a function, an endpoint — receives the data and decides what to do with it.

Two details matter from the start. First, action is optional: leave it off and the form submits to the URL of the page it’s on. Second, action only describes where the data goes. How it’s sent — in the URL or in the request body — is controlled by the method attribute. The two are a pair, so it’s worth being precise about how they interact. If the <form> element itself is new to you, our breakdown of what an HTML form is covers the surrounding anatomy.

Action and method work together

action is the destination; method is the delivery mechanism. The same action URL behaves very differently depending on the method, so picking the method deliberately is half the job. We go deep on this in GET vs POST, but here’s what you need for action.

GET puts data in the URL

With method="get" — which is also the default if you don’t specify a method — the browser serializes the fields into a query string and attaches them to the action URL:

<form action="/search" method="get">
  <input name="q" type="search">
  <button type="submit">Search</button>
</form>

Searching for forms sends the user to:

/search?q=forms

One sharp edge worth knowing: the data replaces the action URL’s existing query string rather than merging with it. If you write action="/search?type=blog" and submit a GET form, the ?type=blog is overwritten by the form’s own fields — it does not survive alongside them. GET is the right call for searches, filters, and anything the user might want to bookmark or reload.

POST puts data in the body

With method="post", the fields travel in the request body instead of the URL:

<form action="/contact" method="post">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Nothing appears in the address bar. POST is the method for submissions that do something — contact forms, signups, orders, uploads. One more thing rides on the method: enctype, which sets how the body is encoded, only takes effect for POST. The default is application/x-www-form-urlencoded, and you must switch to multipart/form-data if the form has a file input, or the file’s contents never get sent. (More on that in uploading files via your form.)

Rule of thumb: GET for retrieving, POST for submitting. Never send passwords or personal data through a GET query string — it ends up in history, logs, and shared links.

What you can put in the action attribute

The action value is a URL, and you have a few forms to choose from. Each resolves relative to the current document, so relative paths behave the way they do in an <a href>.

Value in action What it means Example
Root-relative path Resolves from the site root action="/contact"
Path-relative URL Resolves from the current page’s folder action="submit.php"
Absolute URL Sends to any domain action="https://www.formbackend.com/f/abc123"
Empty / omitted Submits to the current page’s own URL action="" or no action

Because relative values resolve against the document, a <base href> in your page’s <head> will change where they point — a subtle gotcha if you use one. When in doubt, a root-relative path (/contact) or a full absolute URL removes the ambiguity. Absolute URLs are also what make hosted endpoints possible: your form sits on your domain and posts to someone else’s.

Empty vs. missing action

These two look equivalent, and in the browser they behave the same — both submit to the current page’s URL. But they’re not equal under the standard, and the distinction occasionally bites:

<!-- Works, but technically non-conforming HTML -->
<form action="" method="post"> ... </form>

<!-- The standards-compliant way to submit to the same page -->
<form method="post"> ... </form>

The HTML standard asks action to be a valid non-empty URL, so an HTML validator will flag action="". Browsers process it anyway by falling back to the document URL, which is why so much code in the wild uses it without apparent issue. If you want a form to post to its own page, just leave action off entirely — same result, no validator warning. And remember that a POST back to the same page is what triggers the browser’s “Confirm Form Resubmission” prompt on reload, which is usually a reason to send the data somewhere else.

What goes in action when you have no server

This is the question the classic examples leave out. They assume action="/process.php" — a script on a server you control. But a huge amount of the modern web is static: a site built with Astro, Hugo, Eleventy, or plain HTML on a CDN, with no backend to receive a POST. So what URL goes in action?

You can’t point action at a file that isn’t there. What you can do is point it at a URL that’s able to accept the submission on your behalf. There are three real options.

Option What you put in action Good when
Hosted form endpoint An absolute URL to the service You want submissions, email, and spam filtering with zero backend code
Serverless function A route like /api/contact You already deploy functions on Netlify, Vercel, or Cloudflare
JavaScript fetch() Nothing — JS intercepts and posts You need a custom async flow with no page reload

Option 1: a hosted form endpoint

A form backend gives you a URL that’s built to receive form posts. You point action at it, and the service stores the submission, emails you, filters spam, and can forward the data onward. This is exactly what FormBackend is — the action attribute is the integration. There’s no SDK and no JavaScript required:

<form action="https://www.formbackend.com/f/your-form-endpoint" method="post">
  <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" required></textarea>

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

When someone submits, the browser POSTs the fields to that absolute URL, FormBackend records the submission and sends you a notification, then redirects the visitor to a thank-you page (which you can point anywhere you like). Because it’s a plain HTML form posting to a URL, it works identically on a static site, inside a framework, or in a no-code builder. You can create one in a couple of minutes by following your first form.

Option 2: a serverless function

If you already deploy serverless functions, action can point at a function route on the same site:

<form action="/api/contact" method="post">
  <input name="email" type="email" required>
  <button type="submit">Notify me</button>
</form>

Your function at /api/contact receives the POST and does the work — validation, storage, sending mail. This keeps everything in your own infrastructure, at the cost of writing and maintaining that function (and its email sending, spam handling, and storage).

Option 3: intercept with JavaScript

When you want an async submit with no page navigation, let the markup carry a fallback action and intercept the submit in JavaScript:

<form id="contact" action="https://www.formbackend.com/f/your-form-endpoint" method="post">
  <input name="email" type="email" required>
  <button type="submit">Send</button>
</form>

<script>
  const form = document.getElementById('contact');
  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    const response = await fetch(form.action, {
      method: 'POST',
      body: new FormData(form),
      headers: { 'Accept': 'application/json' }
    });
    if (response.ok) form.reset();
  });
</script>

Note that form.action reads straight from the attribute, so the same URL drives both the no-JS fallback and the fetch() call. Two things to watch with this approach: the endpoint must return the right CORS headers since you’re posting cross-origin, and event.preventDefault() means you are now responsible for actually sending the request. Our guide to submitting forms with JavaScript walks through the full pattern.

Route one form to different URLs with formaction

A form has one action, but a single submit button can override it. The formaction attribute on a <button> or <input type="submit"> replaces the form’s action for that button only. That lets one form send data to different URLs depending on which button was clicked:

<form action="/posts/save" method="post">
  <textarea name="body"></textarea>

  <button type="submit">Save draft</button>
  <button type="submit" formaction="/posts/publish">Publish</button>
</form>

Click Save draft and the data goes to /posts/save. Click Publish and the same fields go to /posts/publish instead. No JavaScript, no duplicate forms. formaction also works on <input type="submit"> and <input type="image">, and it’s handy any time an action has two outcomes — save vs. delete, approve vs. reject — that you’d rather not branch in server code.

Why your form action isn&#39;t working: a checklist

When a form submits to nothing, reloads the page, or drops half its data, the cause is almost always one of a handful of things. Run down this list:

Symptom Likely cause Fix
Some fields are missing on the server Inputs have no name attribute Every field you want submitted needs name="..."id alone is not sent
File upload arrives empty Wrong encoding Add enctype="multipart/form-data" and use method="post"
Page reloads, nothing happens action points at a 404, or method is GET when it should be POST Verify the URL loads and the method matches the endpoint
Submit does nothing at all A JS handler calls preventDefault() but never sends the request Make sure your fetch() runs, or remove the handler
Cross-origin POST is blocked Missing CORS headers on the endpoint Use a normal form submit, or ensure the endpoint allows your origin
Data overwrites your query string GET form with params already in action Use POST, or move those params into hidden inputs
A button submits when you didn’t expect it A <button> with no type defaults to type="submit" Set type="button" on any button that shouldn’t submit
action is ignored entirely The form uses method="dialog" Use get or post if you want the data sent to a URL

The single most common one is the first: an input without a name is invisible to the submission, no matter how it looks on screen. If the form posts but the values are wrong or empty, check name attributes before anything else. Validation issues are a close second — our guide to client-side validation covers why a form might refuse to submit before it ever reaches the action URL.

The short version

action is the URL that receives your form’s data, and method decides whether it rides in the URL (GET) or the body (POST). Leave action off to submit to the current page; set it to an absolute URL to submit anywhere. The part the reference docs skip is the one that matters most on a modern static site: if you don’t run a server, point action at something that can receive the POST for you — a serverless function, your own fetch() call, or a hosted endpoint.

That last option is the fastest. If you want to give any HTML form a real backend without writing one, FormBackend hands you a URL to drop into your form’s action. Point your form at it and you get stored submissions, email notifications, spam filtering, redirects, and integrations — no server code, on any site. It’s a few minutes of work to wire up a real contact form and start collecting submissions.

Add a form backend to your site in minutes

Connect any HTML form to FormBackend and start collecting submissions — no backend code required.

Start free