SAE Academy
Forms

Form Basics

How HTML forms work, what the form element's attributes control, and how to group related inputs.

A form is a collection of controls that the user fills out and submits. The browser collects all the values and sends them somewhere -- a server, a URL, a JavaScript handler. Without the <form> element wrapping those controls, that collection and sending mechanism does not exist.

The <form> element

What breaks without it

Place inputs and a submit button on a page with no <form> wrapper:

<!-- No form wrapper -->
<input type="text" name="name" placeholder="Your name">
<input type="email" name="email" placeholder="Your email">
<button type="submit">Send</button>

Clicking the button does nothing useful. There is no form boundary for the browser to collect from, no destination for the data, and no submission event to intercept with JavaScript. The inputs look correct visually. They are broken functionally.

Wrap them in <form> and the browser has everything it needs:

<form action="/contact" method="post">
  <input type="text" name="name" placeholder="Your name">
  <input type="email" name="email" placeholder="Your email">
  <button type="submit">Send</button>
</form>

The action attribute

action tells the browser where to send the form data. Its value is a URL.

<!-- Send to a server endpoint -->
<form action="/submit-contact" method="post">

<!-- Send to an absolute URL -->
<form action="https://example.com/api/contact" method="post">

If you omit action, the browser submits to the current page's URL. In practice, most modern web apps handle form submission with JavaScript and the action attribute is less critical. For any HTML form that submits without JavaScript, action is required.

The method attribute

method controls how the data is sent. There are two values: get and post.

method="get" appends the form data to the URL as query parameters:

https://example.com/search?query=blue+ceramic+mug&category=homeware

Use get for search forms and filters. The resulting URL can be bookmarked, shared, and navigated back to -- the user's search is preserved in the URL. Never use get for passwords, credit cards, or any sensitive data. The values appear in the URL bar, browser history, and server logs.

method="post" sends the data in the request body, not in the URL. Use post for login forms, sign-up forms, file uploads, and anything that modifies data on the server. The submitted values are not visible in the URL.

<!-- Good: search form. URL becomes /search?query=... -->
<form action="/search" method="get">
  <input type="search" name="query">
  <button type="submit">Search</button>
</form>

<!-- Good: login form. Password not exposed in URL -->
<form action="/login" method="post">
  <input type="email" name="email">
  <input type="password" name="password">
  <button type="submit">Log in</button>
</form>

The enctype attribute

enctype controls how the form data is encoded before sending. You only need to set it when the form includes a file upload.

The default value is application/x-www-form-urlencoded, which works for all text fields. For file inputs, you must use multipart/form-data:

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar">
  <button type="submit">Upload</button>
</form>

Without enctype="multipart/form-data" on a file upload form, the browser sends the filename instead of the file contents. The upload appears to work but nothing actually arrives on the server.

The novalidate attribute

Browsers run their own validation before submitting a form. An <input type="email"> with an invalid value causes the browser to block submission and show an error bubble. novalidate turns that off:

<form action="/contact" method="post" novalidate>

Use novalidate when you want to validate fields yourself in JavaScript and show custom error messages. Without it, you would fight the browser's default error bubbles.

How submission works

When a user submits a form, the browser collects the name/value pairs of all successful controls. A control is successful if:

  • It has a name attribute
  • It is not disabled
  • For checkboxes and radio buttons, it is checked

If a control has no name attribute, its value is not submitted -- even if the user filled it in.

<form action="/contact" method="get">
  <!-- Submitted: name="first-name", value="Alice" -->
  <input type="text" name="first-name" value="Alice">

  <!-- NOT submitted: no name attribute -->
  <input type="text" value="This is lost">

  <!-- NOT submitted: disabled -->
  <input type="text" name="last-name" value="Smith" disabled>

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

With method="get", you can see exactly what was submitted by looking at the URL after submission: ?first-name=Alice. This is a useful way to debug forms while learning.

Fieldset and legend

The problem with ungrouped controls

Consider a form asking how the user prefers to be contacted. Without grouping:

<label for="contact-email">Email</label>
<input type="radio" id="contact-email" name="contact" value="email">

<label for="contact-phone">Phone</label>
<input type="radio" id="contact-phone" name="contact" value="phone">

<label for="contact-post">Post</label>
<input type="radio" id="contact-post" name="contact" value="post">

A screen reader focuses on the first radio button and reads: "Email, radio button, 1 of 3." The user has no idea what question these three options are answering. The group question -- "Preferred contact method" -- is nowhere in the HTML.

Grouping with <fieldset> and <legend>

<fieldset> wraps a group of related controls. <legend> provides the label for the group. Together, they give the group a title that screen readers announce before each option.

<fieldset>
  <legend>Preferred contact method</legend>

  <label>
    <input type="radio" name="contact" value="email"> Email
  </label>
  <label>
    <input type="radio" name="contact" value="phone"> Phone
  </label>
  <label>
    <input type="radio" name="contact" value="post"> Post
  </label>
</fieldset>

A screen reader now reads: "Preferred contact method. Email, radio button, 1 of 3." The user knows the question and the options.

Use <fieldset> for radio button groups, checkbox groups, and logical sections of longer forms (billing address, shipping address). Do not use <fieldset> to wrap a single input -- it is for groups.

What goes wrong without legend

If you use <fieldset> but omit <legend>:

<!-- Missing legend: the fieldset creates a visual box but adds no accessible label -->
<fieldset>
  <input type="radio" name="contact" value="email">
  <label for="contact-email">Email</label>
</fieldset>

The fieldset wraps the controls but the screen reader still has no group label. The visual border is there. The accessibility is not.

The <output> element

<output> is a semantic element for displaying a result that comes from the form's controls. The browser treats it as a live region -- screen readers announce changes to it.

Associate it with its source controls using the for attribute, listing the IDs of the relevant inputs:

<form>
  <label for="volume">Volume</label>
  <input type="range" id="volume" name="volume" min="0" max="100" value="50"
    oninput="volumeDisplay.value = this.value">

  <output id="volumeDisplay" for="volume">50</output>
</form>

Without <output>, a <span> updated by JavaScript looks identical visually but carries no semantic meaning. Screen readers do not know the span is a form result. <output> communicates the relationship between the slider and the displayed value.

Common mistakes

Inputs placed outside a <form> element. They render and accept input normally. The submit button does nothing useful. Always check that every input is a descendant of a <form>.

Using method="get" for login or sign-up forms. The password appears in the URL. Use method="post" for any form containing sensitive data or that creates or modifies data on the server.

Missing name attributes on inputs. The input looks normal. The value is silently dropped from the submission. Every input you want to submit must have a name.

Missing enctype="multipart/form-data" on file upload forms. The upload appears to succeed. The server receives the filename, not the file. Always add enctype="multipart/form-data" when the form contains <input type="file">.

Using <fieldset> without <legend>. The visual box is there. The accessible group label is not. Every <fieldset> needs a <legend>.

Not using <fieldset> for radio or checkbox groups. Each option's label identifies the option, not the question. Screen reader users hear isolated options with no context.

Exercise

Build a contact form using a GET method so you can see the submitted data appear in the URL.

  1. Wrap all inputs in a <form> with method="get".
  2. Add a text input for name and an email input for email address. Give each a proper <label>.
  3. Add a <fieldset> with a <legend> containing three radio buttons for "How did you hear about us?" with options: Search engine, Social media, Word of mouth.
  4. Add a <textarea> for the message.
  5. Add a submit button. After submitting, check the URL -- you should see the form values as query parameters.

On this page