skip to content

Submission Semantics and FormData

What the browser does on submit: which method and encoding it uses, which button was pressed, and how controls are serialized by name. FormData and the formdata event are the bridge between native submission and fetch-based flows.

part ofHTMLoverview, primer and where to startread it →
on this pageshow

questions

6

When a browser submits a plain HTML form, which controls actually end up in the submitted data, and what excludes a control from it?

level: juniorimportance: must knowfreq 62%

answer

  1. no name, no entry
  2. id is for labels, not the wire
  3. disabled is skipped, readonly is not
  4. unchecked boxes send nothing at all
  5. the form attribute joins from outside

basics

~20 s

Only enabled controls that have a non-empty name attribute are submitted. Missing name, disabled, and unchecked checkboxes or radios all send nothing; readonly fields are still sent, and a checkbox with no value attribute sends the string "on".

solid answer

~50 s

On submit the browser builds an **entry list** of name/value pairs from the form's controls, and everything else — the query string for GET, the request body for POST — is just a serialization of that list. A control joins the list only if it has a non-empty `name`; `id` is for labels and scripts and never appears in the payload. `disabled` controls are skipped entirely, while `readonly` ones are still submitted. Unchecked checkboxes and unchecked radios contribute nothing at all, which is why servers must treat "absent" as false rather than expecting `false`; a checked checkbox with no `value` attribute submits `on`. Several controls may share a name — a radio group produces one entry, a checkbox group produces several. And a control does not have to sit inside the `<form>` tags: `form="formId"` attaches it from anywhere in the document.

go deeper

for a junior

Be ready to say plainly that a control needs a name attribute to be submitted, that id is not it, and that disabled controls and unchecked checkboxes send nothing.

for a middle

Explain the entry-list step that happens before any encoding, and walk through the exclusions and their consequences: readonly still submits, an unnamed field vanishes silently, a valueless checkbox sends on.

for a senior

Show how you debug a missing field in a real form — read the request payload, rebuild the entry list in the console — and explain why readonly and disabled are presentation, never a server-side guarantee.

for a principal

Own the convention: agree how absent-means-false fields are represented across the codebase, whether hidden companion inputs or an explicit field manifest, so back ends stop inventing per-endpoint rules for the same markup.

## Submission is a two-step process It helps to separate the two things the browser does. First it **constructs the entry list**: an ordered list of name/value pairs gathered by walking the form's controls in tree order. Only then does it **serialize** that list — into a URL query string for `method="get"`, into a percent-encoded or multipart body for `method="post"`. Almost every "my field didn't arrive" bug is a step-one bug, so the useful mental model is: *what gets into the entry list?* ## The name attribute is the key, not id A control contributes an entry only if its `name` attribute is present and non-empty. `id` plays no part in submission — it exists so a `<label for>` can point at the control and so scripts and fragment links can find it. ```html <label for="email">Email</label> <input id="email" name="email" type="email"> ``` Here `email=…` is submitted because of `name`, not because of `id`. Drop the `name` and the field silently vanishes from the payload while the page keeps looking correct — one of the most common form bugs there is. ## What is excluded, and what only looks excluded - **`disabled`** — excluded. A disabled control produces no entry (it is also skipped by validation). If you want the value greyed out *and* sent, use `readonly` plus a hidden field, or re-enable the control just before submit. - **`readonly`** — included. Readonly means the user cannot edit it, not that the server won't see it. Never rely on `readonly` for anything security-relevant; the value is still user-controlled at the wire. - **Unchecked checkbox or radio** — no entry. There is no `false`, no empty string, nothing. Server code must treat absence as "off". A common companion trick is a hidden input with the same name placed *before* the checkbox, so the server always receives at least one value. - **A checked checkbox with no `value`** — submits the string `on`. That default surprises people who expect `true`; give the checkbox an explicit `value` when the wire format matters. - **Buttons** — only the button that actually triggered the submission contributes its `name`/`value`. Other submit buttons, and every `type="button"` or `type="reset"` button, contribute nothing. - **`type="hidden"`** — always included when it has a name. That is exactly its job: carrying state such as a record id through a round trip. ## Repeated names are normal Names are not required to be unique. A radio group is *defined* by a shared name, and it yields a single entry for whichever radio is checked. A checkbox group with a shared name yields one entry per checked box, in document order: ```html <input type="checkbox" name="topic" value="html" checked> <input type="checkbox" name="topic" value="css"> <input type="checkbox" name="topic" value="a11y" checked> ``` That submits `topic=html&topic=a11y`. Server frameworks surface repeated names as a list, and on the client `FormData.prototype.getAll("topic")` returns both values while `get("topic")` returns only the first. Any code that flattens entries into a plain object — `Object.fromEntries` included — quietly keeps only the last occurrence of each name, so multi-value fields need `getAll`. ## A control does not have to be inside the form element Every control has a **form owner**. Normally that is the nearest ancestor `<form>`, but the `form` attribute overrides it by id: ```html <form id="checkout" action="/orders" method="post"> … </form> <input name="coupon" form="checkout"> ``` The coupon field submits with the checkout form even though it lives elsewhere in the document. This is what saves you when layout or a table structure makes nesting impossible, and it means "is it inside the tags?" is not the real test of membership. ## How to check quickly Two reliable ways to see the truth rather than guess it. In DevTools, submit the form and read the request's form-data payload. In the console, build the same entry list the browser would: ```js const form = document.querySelector("#checkout"); for (const [name, value] of new FormData(form)) { console.log(name, value); } ``` If a field is missing from that loop, it will be missing on the server too, and the cause is almost always a missing `name` or a stray `disabled`.

  • How should a server distinguish "the user unchecked this box" from "this field was never on the form"?
    It cannot, from the payload alone — both look identical, since an unchecked box sends nothing. You make it explicit in the markup: put a hidden input with the same name and a falsy value immediately before the checkbox, so an unchecked box yields just the hidden value and a checked one yields both, with the later entry winning. Alternatively, send an explicit list of the fields the form contained.
  • A field is greyed out with disabled but the team still needs its value server-side. What do you do?
    Do not rely on `disabled` — it removes the control from the entry list. Either use `readonly`, which keeps the value in the payload while blocking editing, or keep the visible control disabled and add a `type="hidden"` input carrying the same name and value. Whichever you choose, revalidate server-side: the client can change either one.
  • Why is it legal, and often intentional, for several controls to share the same name?
    Because the entry list is a list, not a map. A radio group is defined by a shared name — that is what makes the options mutually exclusive — and a checkbox group uses a shared name to submit several values under one key. On the client, read them with `formData.getAll(name)`; flattening into an object keeps only the last one.

saying these in an interview costs you the question

  • Thinks the id attribute is what names the field on the wire
  • Expects an unchecked checkbox to submit false or off
  • Thinks readonly fields are omitted just like disabled ones
  • Assumes every button in the form submits its name and value
  • Believes a control must be nested inside form tags to participate

context

open as a page

An HTML form contains <input type="file">. Why must it set enctype="multipart/form-data", and what does the browser send differently from the default encoding?

level: middleimportance: must knowfreq 56%

basics

~20 s

The default encoding, application/x-www-form-urlencoded, can only carry percent-encoded text pairs, so a file input contributes just its filename. multipart/form-data splits the submission into labelled parts separated by a boundary, each able to carry raw bytes plus a filename and its own content type.

open as a page

What changes when an HTML <form> uses method="get" instead of method="post", and how do you choose between them?

level: middleimportance: must knowfreq 70%

basics

~20 s

With method="get" the browser puts the form's entries in the URL query string, so the result is bookmarkable and reloadable; with method="post" it puts them in the request body, which supports file uploads and keeps values out of the URL and history.

open as a page

An HTML form has two submit buttons, Save and Publish. How does the server know which one the user pressed, and how can one button post to a different URL than the form's action?

level: middleimportance: should knowfreq 46%

basics

~20 s

Only the activated submit button contributes its name and value to the submission, so giving each button a name and a distinct value tells the server which was pressed. A button's formaction attribute overrides the form's action URL for that button only.

open as a page

Without using any framework, how do you take over a form's submission and send it with fetch while sending exactly the data the browser would have sent, and what is the form's formdata event for?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Listen for the form's submit event, prevent the default navigation, and build new FormData(form, event.submitter) — that reproduces the browser's own entry list. The formdata event fires whenever that list is built, letting a handler append entries for state no control holds.

open as a page

In a signup form, pressing Enter in the email field submits the form, and clicking a "Show password" <button> inside it reloads the page. Explain both behaviours and how you would control them.

level: seniorimportance: should knowfreq 52%

basics

~20 s

Both are default form behaviour. A <button> with no type attribute defaults to type="submit", so the show-password button submits and navigates. Enter in a text field triggers implicit submission, which activates the form's first submit button. Fix the button with type="button"; keep Enter working.

open as a page