#How a form submits
AddedA <form> is the web's original API client. When it's submitted, the browser walks its controls, collects a name/value pair for every control that's eligible, encodes the list, and navigates to the form's action URL with it: as a query string for method="get", or in the request body for method="post". No JavaScript is involved, which is why plain forms work before your scripts load, with scripts blocked, and in every browser.
Which controls get sent is the part that trips people up. A control is included only if it has a name, isn't disabled, and (for checkboxes and radios) is checked. A checkbox with no value attribute sends the value on. A submit button is included only if it's the one that submitted the form. Read-only fields are sent; disabled fields are not. The order is the order of the controls in the document.
Mental model: the form is a courier filling in a delivery slip. It writes one line per labelled parcel (name=value), skips anything without a label (no name), anything taped shut (disabled) and any empty checkbox, and the method decides whether the slip is stuck on the outside of the envelope (GET, visible in the URL) or sealed inside (POST, in the body).
form { display: grid; gap: 6px; max-width: 360px; padding: 10px; border: 1px solid #c7cfe0; border-radius: 8px; }
label { display: flex; gap: 6px; align-items: center; }
input:not([type=checkbox]), select { font: inherit; padding: 3px 6px; flex: 1; }
.off { color: #6b7280; }<form id="search" action="/search" method="get">
<label>Search <input name="q" value="red shoes & socks"></label>
<label><input type="checkbox" name="in_stock" checked> In stock only</label>
<label><input type="checkbox" name="sale"> On sale</label>
<label>Sort
<select name="sort">
<option value="price">Price</option>
<option value="new" selected>Newest</option>
</select>
</label>
<input type="hidden" name="ref" value="nav">
<label class="off">No name: <input value="never sent"></label>
<label class="off">Disabled: <input name="coupon" value="SAVE10" disabled></label>
<button name="view" value="grid">Search</button>
<button name="view" value="list">Search (list view)</button>
</form>- The test presses the first "Search" button and catches the navigation the form starts (with the Navigation API, then cancels it). The browser navigates to
/search?q=red+shoes+%26+socks&in_stock=on&sort=new&ref=nav&view=grid. The live result on this page is sandboxed, so pressing the button there does nothing; the test shows what would be sent. qis encoded asapplication/x-www-form-urlencoded: spaces become+, and&becomes%26so it can't be confused with the separator between pairs. The checked checkbox has novalue, so it sendsin_stock=on; the uncheckedsalesends nothing at all (notsale=off), so the server must treat a missing key as "false".- The
selectsends its selected option'svalue(new, not the text "Newest"). The hidden input is sent like any other (ref=nav); hidden doesn't mean secret, it's in the URL for anyone to see. - The input with no
nameand the disabledcouponare both missing.new FormData(form)gives the same list the browser uses, which is why it's the standard way to read a form in JavaScript. - The submit button is included only as the submitter:
view=gridwhen the first button is pressed, andnew FormData(form, submitter)with the second button givesview=list. Two buttons with the samenameand different values are a neat way to offer two actions in one form.
method="get" (the default) | method="post" | |
|---|---|---|
| Where the data goes | query string of the URL | request body |
| Visible in URL, history, logs | yes | no |
| Bookmarkable, shareable, cacheable | yes | no |
| Size | limited by URL length (servers commonly cap at a few KB) | large; files possible |
| Meaning | "read something" (search, filters), safe to repeat | "change something" (sign in, buy, post), not safe to repeat |
| Back / refresh | reloads the URL | browser warns about resubmitting |
Use GET for searches and filters, so the result has a URL you can share. Use POST for anything that changes state or contains sensitive data; a password in a GET URL ends up in the browser history and server logs. After a successful POST, servers usually reply with a redirect (the Post/Redirect/Get pattern) so refreshing doesn't submit twice.
form { margin-bottom: 8px; padding: 8px; border: 1px solid #c7cfe0; border-radius: 8px; }<form id="login" action="/session" method="post">
<input name="email" value="rohit@example.com"> <input type="password" name="password" value="open sesame"> <button>Sign in</button>
</form>
<form id="upload" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="avatar"> <button>Upload</button>
</form>
<form id="odd" action="/x" method="put" enctype="application/json"></form>- Submitting the login form navigates to
/sessionwith no query string: with POST the pairs go in the request body. The body is the same urlencoded format as a GET query,email=rohit%40example.com&password=open+sesame(@is encoded as%40). It's hidden from the URL, not encrypted; only HTTPS protects it in transit. enctypechooses the body format. The default isapplication/x-www-form-urlencoded. Files needmultipart/form-data, which sends each field as its own part with headers, so binary data can travel unencoded. With the default enctype a file input only sends the file's name, which is the classic "my upload is empty on the server" bug.- In
FormData, a file input is aFileobject. With nothing chosen, it's still an entry: aFilewith an empty name and size0, so servers should check the size, not just the key. - HTML forms only know
get,postanddialog.method="put"is invalid, soform.methodfalls back to"get", andenctype="application/json"falls back to the default too. To send PUT, DELETE or JSON you need JavaScript (fetch), or a framework convention like a hidden_methodfield.
requestSubmit() vs submit()form.addEventListener("submit", async (event) => {
event.preventDefault(); // stop the navigation
const data = new FormData(form, event.submitter);
await fetch(form.action, { method: "POST", body: data }); // multipart body
});
form.requestSubmit(); // like clicking a submit button: validates, fires "submit"
form.submit(); // skips validation AND the submit event, navigates directly<form id="f" action="/order">
<input name="email" required placeholder="email (required, left empty)">
</form>- The form has one required field, left empty.
requestSubmit()behaves exactly like a user pressing a submit button: it runs validation, finds the empty field, firesinvalidon it, and stops. Nosubmitevent, no navigation. submit()is the old method: it skips validation and never fires thesubmitevent, so yoursubmitlistener (and anypreventDefault()in it) is bypassed. The test sees no new events, yet the browser navigates to/orderwith the invalid data.- That's why
requestSubmit()(Baseline since 2022) is the one to use when code needs to submit a form, for example on a keyboard shortcut. In asubmithandler,event.submittertells you which button was pressed, andnew FormData(form, event.submitter)includes it. - If you
fetchaFormDataobject as the body, the browser sends it asmultipart/form-dataand sets the boundary header itself; don't setContent-Typemanually. Usenew URLSearchParams(formData)as the body if the server expects urlencoded.
<form action="/prefs" method="get">
<input type="checkbox" name="email_updates" value="yes">
<input type="checkbox" name="topics" value="css" checked>
<input type="checkbox" name="topics" value="html" checked>
<input name="nickname" value="Ro" readonly>
<input name="plan" value="pro" disabled>
<button>Save</button>
</form>Show answer
/prefs?topics=css&topics=html&nickname=Ro
email_updatesis unchecked, so it's left out entirely; the server never sees the key.- Both
topicscheckboxes are checked, so the key appears twice:topics=css&topics=html. Servers differ in how they read repeated keys (PHP needs the nametopics[]; Express withqsgives an array;URLSearchParams.getreturns only the first,getAllreturns both). nicknameisreadonly, which only stops editing: it's still submitted.planisdisabled, so it's excluded. If you want a field shown but not editable and sent, usereadonly.- The button has no
name, so it adds nothing. This is the same rule set the demo above measured: name required, not disabled, checked if checkable.