---
title: 'Form Structure'
source: 'https://academia.sh/en/courses/web-fundamentals-and-html/form-structure'
course: 'Web Fundamentals and HTML'
language: en
updated: '2026-08-17T18:09:32+00:00'
license: 'CC BY-SA 4.0'
---

# Form Structure

The entry list a submission produces, the effect the submission method has on the url and the body, the reasoning behind choosing a method, and grouping fields.

The document is now structured, sectioned, and carries media — but it is still one-way. The
reader receives information, but cannot give any back. This topic opens that direction.

A form is the mechanism that turns values taken from the user into a request. The HTTP
Request and Response lesson in the How the Internet Works course defined the parts of a
request — the request line, the headers, the body. This lesson looks at how the values that
feed those parts are collected in the document, and in what shape they are written.

## The Parts of a Form

The `form` element is a unit of submission. Two attributes determine where and how the
submission happens.

`action` is the url the request goes to. If it is not written, the document's own url is
used.

`method` is the HTTP method: `get` or `post`. If it is not written, `get` is assumed.

Of the fields inside a form, only the ones **with a name** are submitted. The `name`
attribute is what enters a field into the submission; a field with no name still appears on
screen and can have a value typed into it, but it never reaches the server.

## The Entry List

When submission starts, the browser walks the fields inside the form and builds an **entry
list** of name-value pairs. This list is ordered and follows the fields' order in the
document; the same name can appear more than once.

Some fields never enter the list: ones with no name, ones that are disabled, and unchecked
checkboxes. This last rule is easy to miss — an unchecked checkbox is not submitted with a
"false" value, it is not submitted at all.

## The Method Determines the Body

Once the entry list is built, where it gets written depends on the method. The script below
opens a local server, makes both submissions, and prints what the server sees.

```js
// submission.mjs — how get and post submissions look on the server
import { createServer } from "node:http";

// The name-value pairs the form produces (the entry list).
const entryList = [
  ["station", "north-slope"],
  ["measurement", "temperature"],
  ["value", "-4.2"],
  ["note", "windy & clear — no rain"],
];

const server = createServer((request, response) => {
  const chunks = [];
  request.on("data", (chunk) => chunks.push(chunk));
  request.on("end", () => {
    const body = Buffer.concat(chunks);
    console.log("request line :", request.method, request.url);
    console.log("content-type :", request.headers["content-type"] ?? "(none)");
    console.log("body         :", body.length ? JSON.stringify(body.toString()) : "(empty)");
    console.log("---");
    response.writeHead(204).end();
  });
});

server.listen(0, "127.0.0.1", async () => {
  const base = "http://127.0.0.1:" + server.address().port;
  const query = new URLSearchParams(entryList).toString();

  // method="get": the action url's query part is erased, replaced with the entry list.
  const url = new URL("/record?old=value", base);
  url.search = query;
  await fetch(url);

  // method="post", default enctype: the entry list is written to the body.
  await fetch(new URL("/record?old=value", base), {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: query,
  });

  server.close();
});
```

```
request line : GET /record?station=north-slope&measurement=temperature&value=-4.2&note=windy+%26+clear+%E2%80%94+no+rain
content-type : (none)
body         : (empty)
---
request line : POST /record?old=value
content-type : application/x-www-form-urlencoded
body         : "station=north-slope&measurement=temperature&value=-4.2&note=windy+%26+clear+%E2%80%94+no+rain"
---
```

The output says four things.

**The `get` method sends no body.** The list is written into the url's query part. As a
result, the values show up in the browser's history, in bookmarks, and in server logs.

**The action url's existing query is erased.** The `old=value` pair from the request line is
gone. Writing a query onto an `action` url does nothing on a form submitted with `get`; it has
to be carried as a hidden field instead.

**The `post` method writes the list to the body** and keeps the url's query intact.

**The encoding is the same either way.** In both cases the list becomes `name=value` pairs
joined with `&`; spaces become `+`, non-ASCII characters and `&` get percent-encoded. The
separator inside `windy & clear — no rain` became `%26`, and the em dash became `%E2%80%94` —
the same percent-encoding as in the Links lesson. This format is called
`application/x-www-form-urlencoded`, announced in the `Content-Type` header on a `post`
submission.

## Choosing a Method

The choice is not a habit, it follows from the nature of the request. The two properties
defined in the HTTP Request and Response lesson apply directly here: a **safe** method makes no
change on the server, an **idempotent** method leaves the same state no matter how many times
it runs.

`get` is safe. It is used only for submissions that make no change on the server: a search, a
filter, a query. That the result can be shared as a url and bookmarked is a gain.

`post` is not safe. Every submission that changes something on the server uses this method:
adding a record, sending a notification. The values do not show up in the url, and the browser
warns before repeating the submission.

Submitting a record with `get` means that sharing the link, or reloading the url, redoes the
operation. This distinction is not a privacy measure — a `post` body is unencrypted too;
protection comes from the transport layer described in the Role of HTTPS lesson.

## Grouping Fields

The `fieldset` element groups fields that belong together, and the `legend` element gives the
group a name, written as the group's first child.

```html
<fieldset>
  <legend>Measurement source</legend>
  <p><label><input type="radio" name="source" value="automatic" checked> Automatic</label></p>
  <p><label><input type="radio" name="source" value="manual"> Entered manually</label></p>
</fieldset>
```

Grouping is not optional for radio buttons. The individual labels say only "Automatic" and
"Entered manually" — it is the `legend` text that says what the group is a choice of. A screen
reader announces the group name along with each option.

The `disabled` attribute written on `fieldset` disables every field inside it, and disabled
fields do not enter the entry list; in a multi-step form, this is how a whole section is shut
off at once.

## The Submit Button

The `button` element's `type` attribute takes three values: `submit` submits the form, `reset`
returns the fields to their initial values, `button` does nothing. A `type` attribute left
unwritten inside a form counts as `submit` — an ordinary button placed inside a form submits it
without anyone intending that.

The `formaction` and `formmethod` attributes override the form's own settings for the
submission made with that button; this is how the same form is submitted to two different urls.

## Fields Outside the Form

Which form a field belongs to usually follows from nesting: every field sitting inside a
`form` element belongs to it. Forms cannot be nested, so this relation is single-valued.

Nesting does not always give the wanted layout — for example, fields inside a table that
belongs to a form outside the table. In this case, the `form` attribute written on the field
announces the id of the form it belongs to.

```html
<form id="correction" action="/measurement/correction" method="post"></form>

<table>
  <tr>
    <th scope="row">Monday</th>
    <td><input form="correction" name="mon" type="number" value="-4"></td>
  </tr>
</table>

<button type="submit" form="correction">Submit correction</button>
```

A submit button can use this attribute too; the form itself stays empty and carries only the
submission settings.

The `autocomplete` attribute controls whether the browser suggests values entered before. When
written at the field level, it also announces the kind of value expected; this announcement is
the one mechanism that makes a form fillable for users with a cognitive or motor difficulty. It
is turned off only for fields that should not be stored on a shared machine.

## In the Station Document

```html
<section aria-labelledby="notice">
  <h2 id="notice">Report a Measurement Correction</h2>

  <form action="/measurement/correction" method="post">
    <fieldset>
      <legend>Measurement information</legend>
      <p><label for="station">Station code</label>
         <input id="station" name="station" type="text" value="north-slope"></p>
      <p><label for="value">Measured value (°C)</label>
         <input id="value" name="value" type="number"></p>
    </fieldset>

    <p><button type="submit">Submit correction</button></p>
  </form>
</section>
```

`post` is chosen as the method because the submission creates a record on the server. The
fields are gathered into a group, and the group is given a name.

## Summary

- A form builds an entry list from its fields; only fields that have a name, are not
  disabled, and are checked enter this list.
- With `get`, the list is written into the url's query part and the action url's existing
  query is erased; with `post` it is written to the body and the url's query is kept.
- The default encoding is the same in both methods: `name=value` pairs, space as `+`, special
  characters percent-encoded.
- The choice of method follows from the safe and idempotent properties; a submission that
  changes something on the server is made with `post`.
- `fieldset` and `legend` are mandatory for option groups: only the group name says what the
  group is a choice of.

## Next Step

This lesson built a form with a single kind of field. But the data to be collected is not
always free text: a number, a date, a time, a choice, a multiple choice, a file. The next
lesson looks at input types, and shows how much of the type information survives submission —
how all of them reach the server — through a real body.
