When a browser submits a plain HTML form, which controls actually end up in the submitted data, and what excludes a control from it?
answer
- no name, no entry
- id is for labels, not the wire
- disabled is skipped, readonly is not
- unchecked boxes send nothing at all
- the form attribute joins from outside
basics
~20 sOnly 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 sOn 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
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.
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.
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.
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