HTML Form Action Attribute
A form collects data. But where does that data go when the visitor presses Submit? That is the job of the action attribute. It holds the address (URL) of the page or program that will receive and handle the form.
It looks like a small detail, but the address you choose decides who receives your visitors' information, and one small slip can send private data to the wrong place or lose it entirely. In this lesson, you will learn how to write the address, how the browser works it out, and the traps to avoid.
What You Will Learnβ
- What
actiondoes, and what happens when you leave it out - Absolute and relative addresses, and how the browser resolves them
- The GET query string trap
- Overriding the action with
formaction - Opening the result in a new tab with
target - Third-party form services and
mailto: - Adding JavaScript the safe way (progressive enhancement)
- Security and privacy notes
The Basicsβ
- HTML Code
- Browser Output
<form action="/contact" method="post">
<label for="email">Your email address</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit">Send</button>
</form>
When the visitor presses Send, the browser packs the fields as name=value pairs and sends them to /contact on the same website, using the method you chose (see Form Methods). The server program at that address reads the data, does the work, and replies with a page.
So action answers "where?", and method answers "how?"
What Goes in actionβ
The value is a URL, and the browser resolves it against the address of the current page. These are the common shapes:
| Written as | Meaning | Example (page at https://example.com/shop/cart/index.html) |
|---|---|---|
/checkout | Root-relative: from the site's root | https://example.com/checkout |
pay.php | Relative: from the current folder | https://example.com/shop/cart/pay.php |
../receipt | Relative: one folder up | https://example.com/shop/receipt |
https://pay.example.org/charge | Absolute: a full address, often another website | exactly that address |
//cdn.example.net/form | Protocol-relative: same scheme, another host (avoid) | https://cdn.example.net/form |
?step=2 | Only a query: same page, new query | https://example.com/shop/cart/index.html?step=2 |
| (left out) | The current page's address | https://example.com/shop/cart/index.html |
Try It: See Where the Data Goesβ
Type an action, or press a preset button, and see the full address the browser works out.
- HTML Code
- Browser Output
<!-- Page address: https://example.com/shop/cart/index.html -->
<form action="../receipt" method="post">
<button type="submit">Pay</button>
</form>
const page = "https://example.com/shop/cart/index.html";
console.log(new URL("/checkout", page).href);
// https://example.com/checkout
console.log(new URL("pay.php", page).href);
// https://example.com/shop/cart/pay.php
console.log(new URL("../receipt", page).href);
// https://example.com/shop/receipt
You can also read the resolved address in JavaScript: form.action always returns the full URL, even if the attribute was written short.
Leaving action Outβ
If there is no action, the form is sent to the address of the page it is on. This is common in frameworks where one address shows the form (with GET) and handles it (with POST). Leaving the attribute out is fine. An empty action="" is not valid HTML, even though browsers treat it the same, so leave it out instead.
A Word About <base>β
If the page has a <base href="..."> element, relative addresses are worked out from that base, not from the page's own address. A form that suddenly posts to the wrong place after someone adds <base> is a classic puzzle.
The GET Query String Trapβ
With method="get", the browser replaces any query string already in the action with the form's own fields. Try it:
- HTML Code
- Browser Output
<!-- The ?lang=en is lost on submit with GET -->
<form action="/search?lang=en" method="get">
<input type="text" name="q" value="forms" />
<button type="submit">Search</button>
</form>
<!-- Fix: carry the value as a hidden field -->
<form action="/search" method="get">
<input type="hidden" name="lang" value="en" />
<input type="text" name="q" value="forms" />
<button type="submit">Search</button>
</form>
With GET, put extra values in hidden inputs (see Hidden). With POST, a query string in the action is kept, and the fields travel in the body.
Changing the Action for One Button: formactionβ
A submit button can send the same form somewhere else with formaction. Great for "Save" and "Save and publish," or "Preview":
<form action="/articles/save" method="post">
<label for="title">Title</label>
<input type="text" id="title" name="title" />
<button type="submit">Save draft</button>
<button type="submit" formaction="/articles/publish">Publish</button>
<button type="submit" formaction="/articles/preview" formmethod="get" formtarget="_blank">Preview</button>
</form>
The form* attributes (formaction, formmethod, formenctype, formtarget, formnovalidate) apply to that button only. See Submit and Reset Buttons.
Where the Response Opens: targetβ
By default, the response replaces the current page. The form's target attribute can change this:
target value | Result |
|---|---|
_self (default) | Same tab |
_blank | New tab |
_parent, _top | Parent frame, or the whole window (for frames) |
| A frame name | That <iframe> with that name |
<form action="/report" method="post" target="_blank">...</form>
Opening a new tab can surprise visitors, so use it only when there is a good reason (a printable report, a preview). Modern browsers treat target="_blank" forms as if rel="noopener" was set, so the new page cannot control yours.
Sending to Another Websiteβ
The action can point to a different website. That is how many services work: payment providers, newsletter tools, form-handling services, and search engines.
<form action="https://example-newsletter.com/subscribe/abc123" method="post">
<label for="email">Email address</label>
<input type="email" id="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
Things to remember:
- No "cross-origin" error. Submitting a form to another site is a normal page navigation, so it is not blocked by the browser's CORS rules. That is also why it is risky: the browser will happily send data anywhere the page tells it to.
- You send your visitors' data to that company. Choose providers you trust, name them in your privacy policy, and make sure the form does not collect more than it needs.
- The visitor lands on the other site's page unless the service redirects back. Check the "thank you" flow.
- Use
https. Anhttpaction on anhttpspage triggers warnings, or is blocked.
mailto: Actionsβ
<form action="mailto:hello@example.com" method="post" enctype="text/plain">
This looks easy, but it does not send anything. It opens the visitor's email program (if they have one set up), with the data pasted in, and they must press Send themselves. It fails for people who use web email, and it exposes your address to harvesters. For a real contact form, use a server-side handler or a form service instead.
javascript: Actionsβ
action="javascript:..." is possible but a bad idea. It breaks without JavaScript and is blocked by strict security policies. Use an event listener instead.
Working With JavaScript: Progressive Enhancementβ
A good form works without JavaScript, and gets better with it. Keep a real action and method, then take over with JavaScript when it is available:
<form id="contact" action="/contact" method="post">
<label for="email">Email address</label>
<input type="email" id="email" name="email" required />
<button type="submit">Send</button>
<p id="status" role="status"></p>
</form>
const form = document.getElementById("contact");
const status = document.getElementById("status");
form.addEventListener("submit", async (event) => {
event.preventDefault(); // stop the normal page change
status.textContent = "Sendingβ¦";
try {
const response = await fetch(form.action, { // form.action is the full URL
method: form.method,
body: new FormData(form),
});
if (!response.ok) throw new Error("Server said " + response.status);
status.textContent = "Thank you! We got your message.";
form.reset();
} catch (error) {
status.textContent = "Sorry, that did not work. Please try again.";
}
});
If the script fails to load, the form still submits the normal way. Reading form.action and form.method keeps your HTML as the single source of truth.
Accessibility at a Glanceβ
- Tell visitors what will happen when they submit ("We will email you a receipt"), especially when the data goes to another site.
- Do not open new tabs without warning. Add "(opens in a new tab)" to the button or nearby text.
- After submitting with JavaScript, announce the result in a
role="status"area, and move focus to it if it replaces the form. - Do not lose the visitor's data if sending fails. Keep the fields filled in and show a clear message.
- Make sure the form works with only a keyboard, and that pressing Enter in a field submits it as expected.
- A real destination helps robustness: the form still works with JavaScript off, on slow networks, and in assistive technology that struggles with scripts.
Security and Privacy Notesβ
- Know exactly where the data goes. Treat the
actionlike a destination for money: check it carefully, especially when it is built from variables or user input. - Never build
actionfrom untrusted input. If a page puts a value from the URL straight intoaction, an attacker can send visitors' data to their own server. - Use HTTPS for all form destinations, so data is encrypted in transit. An
httpaction on anhttpspage is a downgrade, and browsers may warn or block it. - POST forms need CSRF protection on the receiving server. Use a CSRF token in a hidden field and
SameSitecookies. (See Form Methods.) - Do not trust the destination page to validate. The receiving server must check every value, as nothing in the browser can be trusted.
- Use a Content-Security-Policy
form-actionrule to list the only addresses your forms may post to, for exampleform-action 'self' https://pay.example.org. This stops injected code from adding a form, or changing an action, to send data to an attacker. - Do not collect secrets in forms that go to third parties unless the service is built for it (a payment provider's hosted fields, for example).
- Check redirects. A server that redirects after a form to an address taken from the request can be abused as an open redirect.
See Secure Form Design.
Global Audience Notesβ
- Data protection laws (such as the GDPR in Europe, and many others worldwide) care where personal data goes. If your action sends data to a service in another country, your privacy policy should say so, and you may need consent.
- Addresses may contain non-English characters. Internationalised domain names and paths are converted by the browser, but test them.
- Slow and unreliable networks are common. Handle failure gracefully, keep the visitor's input, and avoid double submissions.
- Use
lang-aware routes carefully. A relative action likesignupon/fr/contactgoes to/fr/signup, while/signupgoes to the root. Check which one you mean. - Third-party services may be blocked in some countries or by some networks. Offer another way to contact you.
Common Mistakes to Avoidβ
- A GET form with a query string in
action. The query is dropped. Use hidden fields. - A relative action that points to the wrong place because the page lives in a different folder, or has a
<base>. - Forgetting the
action, and wondering why the form reloads the page. - Using
mailto:and expecting a message to arrive. - Putting a user-supplied value in
action. - An
http://action on anhttpspage. - Sending data to a third party without telling visitors.
- Writing
action="javascript:...". - No server-side validation because "the browser checks."
- Opening results in a new tab without warning.
- Using
preventDefault()with no fallback, so the form is dead if the script fails. - No CSRF protection on POST destinations.
Practice Timeβ
Try these in your editor:
- Build a form with
action="/thanks"andmethod="post", and see where it sends you. - Remove the
action, and see which address is used. - Use a relative action like
thanks.html, and then../thanks.html, and compare. - Log
form.actionin the console, and notice it is always the full address. - Make a GET form with
action="/search?lang=en", and watch the query get replaced. Then fix it with a hidden field. - Add a "Preview" button with
formaction,formmethod="get", andformtarget="_blank". - Add
target="_blank"to a form and tell the visitor with text. - Write JavaScript that sends the form with
fetch, and still works if the script is removed. - Write a
Content-Security-Policyheader value with aform-actionrule for your site. - Bonus: list which third-party services your own site's forms send data to, and what each one receives.
Quick Recapβ
Click a question to check your answer.
1. What does the action attribute do?
It gives the address (URL) that receives the form's data when it is submitted. The method attribute decides how the data is sent.
2. What happens if you leave out action?
The form is submitted to the address of the page it is on, including the same path and query string.
3. How are relative addresses worked out?
Against the current page's address (or the <base> element, if there is one). /checkout starts from the site's root, pay.php from the current folder, and ../receipt from one folder up.
4. Why does a query string in action disappear with GET?
With method="get", the browser builds a new query string from the form's fields and replaces the old one. Put extra values in hidden inputs, or use POST.
5. How do you send the same form to different addresses?
Give each submit button its own formaction (and, if needed, formmethod and formtarget).
6. What are the main security rules for action?
Use HTTPS, never build the action from untrusted input, protect POST forms with CSRF tokens, validate on the server, and consider a Content-Security-Policy form-action rule so forms can only post to approved addresses.
Key Takeawaysβ
actionis the address that receives the form data.methodis how it is sent.- Without
action, the form goes to its own page. Relative addresses resolve against the current page (or<base>). - With GET, the form's fields replace any query string in the action. Use hidden inputs.
formactionon a button overrides the address for that button only.targetdecides where the response opens.- Forms can post to other sites with no CORS check, so choose destinations carefully, use HTTPS, and tell visitors where their data goes.
mailto:does not really send, andjavascript:is fragile. Keep a real action and addfetchas an enhancement.- Protect the destination with CSRF tokens, server-side validation, and a
form-actionContent-Security-Policy rule.
Next, learn how to send files and mixed data, in Multipart Form Data, or return to the Forms overview.