Forms and Input Types
Every App You've Ever Used Has Forms
Login, signup, search, checkout, settings, comments, file uploads — they're all forms. Forms are the web's primary input mechanism. Get them right, and your app feels polished and professional. Get them wrong, and users rage-quit on mobile, screen reader users can't complete tasks, and your data is full of garbage.
Here's the thing most developers miss: HTML forms are shockingly powerful without any JavaScript. Built-in validation, type-specific keyboards on mobile, autocomplete, form submission — the browser handles all of it. Most of the JavaScript we write for forms is reinventing what HTML already does.
Think of an HTML form like a paper form at a government office. Each field has a label explaining what to fill in, a specific type (date, number, checkbox, signature), required fields are marked with an asterisk, and there's a submit button at the bottom. If you forget a required field, the clerk points it out before processing. HTML forms work the same way — labels, types, required markers, and built-in validation, all before any JavaScript runs.
The Form Element
<form action="/api/subscribe" method="post">
<label for="email">Email address</label>
<input type="email" id="email" name="email" required>
<button type="submit">Subscribe</button>
</form>
The form element wraps all inputs and handles submission:
action— where to send the data (a URL)method—get(data in URL query string) orpost(data in request body)- When the user clicks submit, the browser collects all named inputs and sends them to the action URL
Without JavaScript, this works. The browser validates, submits, and navigates to the response. In a modern SPA, you often intercept submission with JavaScript, but the HTML structure remains the same.
Labels: The Most Important Form Element
Every input needs a label. No exceptions. Labels tell users (and screen readers) what each field is for.
<!-- Explicit association: for + id -->
<label for="username">Username</label>
<input type="text" id="username" name="username">
<!-- Implicit association: input inside label -->
<label>
Username
<input type="text" name="username">
</label>
The for attribute on the label must match the id on the input. This creates an explicit association. When a user clicks the label, the input gets focused — this dramatically improves usability on mobile where small inputs are hard to tap.
Using placeholder as a replacement for labels is one of the most common accessibility violations on the web. Placeholders disappear when the user starts typing, leaving them with no indication of what the field is for. Users with cognitive disabilities or poor short-term memory are particularly affected. Always use a visible label. Placeholders can supplement labels but never replace them.
Input Types
HTML provides specialized input types that change behavior, keyboard, and validation:
<!-- Text inputs -->
<input type="text" name="name"> <!-- Generic text -->
<input type="email" name="email"> <!-- Email validation + @ keyboard on mobile -->
<input type="password" name="pass"> <!-- Masked input -->
<input type="url" name="website"> <!-- URL validation -->
<input type="tel" name="phone"> <!-- Phone keyboard on mobile -->
<input type="search" name="q"> <!-- Search field (clear button on some browsers) -->
<!-- Numeric inputs -->
<input type="number" name="age" min="0" max="120" step="1">
<input type="range" name="volume" min="0" max="100">
<!-- Date and time -->
<input type="date" name="birthday"> <!-- Date picker -->
<input type="time" name="alarm"> <!-- Time picker -->
<input type="datetime-local" name="meeting">
<!-- Selection inputs -->
<input type="checkbox" name="agree"> <!-- Toggle on/off -->
<input type="radio" name="plan" value="free"> <!-- One of many -->
<input type="color" name="theme"> <!-- Color picker -->
<input type="file" name="avatar" accept="image/*">
<!-- Hidden and special -->
<input type="hidden" name="csrf" value="abc123">
The mobile keyboard difference is huge. type="email" shows an @ key. type="tel" shows a number pad. type="url" shows a .com key. Using the right type makes forms dramatically easier to fill on phones.
Built-In Validation
HTML provides powerful validation without writing any JavaScript:
<form action="/api/register" method="post">
<!-- Required field -->
<label for="name">Full name</label>
<input type="text" id="name" name="name" required>
<!-- Email with pattern -->
<label for="email">Work email</label>
<input
type="email"
id="email"
name="email"
required
pattern=".+@company\.com"
title="Must be a @company.com email"
>
<!-- Number with range -->
<label for="age">Age</label>
<input type="number" id="age" name="age" min="18" max="99" required>
<!-- Text with length constraints -->
<label for="bio">Bio</label>
<textarea id="bio" name="bio" minlength="10" maxlength="500"></textarea>
<button type="submit">Register</button>
</form>
Validation attributes:
required— field must have a valuepattern— value must match a regexmin/max— numeric or date rangeminlength/maxlength— text length rangetitle— shown in the validation tooltip (describes the expected format)
The browser shows native error messages when validation fails. You can style valid/invalid states with CSS:
input:valid {
border-color: green;
}
input:invalid {
border-color: red;
}
/* Only show error styling after the user has interacted */
input:not(:placeholder-shown):invalid {
border-color: red;
}
Selects, Textareas, and Fieldsets
Select Dropdowns
<label for="country">Country</label>
<select id="country" name="country" required>
<option value="">Choose a country</option>
<optgroup label="North America">
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="mx">Mexico</option>
</optgroup>
<optgroup label="Europe">
<option value="uk">United Kingdom</option>
<option value="de">Germany</option>
<option value="fr">France</option>
</optgroup>
</select>
The first option with an empty value acts as a placeholder. optgroup groups related options visually and semantically.
Textareas
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
cols="40"
placeholder="Write your message here..."
required
minlength="20"
></textarea>
Unlike input, textarea is not a void element — it has a closing tag. Content between the tags is the initial value.
Fieldsets and Legends
Group related fields with fieldset and label the group with legend:
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input type="text" id="street" name="street" required>
<label for="city">City</label>
<input type="text" id="city" name="city" required>
<label for="zip">ZIP Code</label>
<input type="text" id="zip" name="zip" pattern="[0-9]{5}" required>
</fieldset>
fieldset is especially important for radio button groups — the legend provides the question, and each radio's label provides the answer:
<fieldset>
<legend>Select your plan</legend>
<label><input type="radio" name="plan" value="free"> Free</label>
<label><input type="radio" name="plan" value="pro"> Pro ($29/mo)</label>
<label><input type="radio" name="plan" value="team"> Team ($99/mo)</label>
</fieldset>
Production Scenario: Accessible Registration Form
<form action="/api/register" method="post" novalidate>
<h2>Create your account</h2>
<div>
<label for="reg-email">Email address</label>
<input
type="email"
id="reg-email"
name="email"
required
autocomplete="email"
aria-describedby="email-hint"
>
<p id="email-hint">We will never share your email.</p>
</div>
<div>
<label for="reg-pass">Password</label>
<input
type="password"
id="reg-pass"
name="password"
required
minlength="8"
autocomplete="new-password"
aria-describedby="pass-requirements"
>
<p id="pass-requirements">At least 8 characters, one uppercase, one number.</p>
</div>
<fieldset>
<legend>Account type</legend>
<label>
<input type="radio" name="type" value="personal" checked>
Personal
</label>
<label>
<input type="radio" name="type" value="business">
Business
</label>
</fieldset>
<div>
<label>
<input type="checkbox" name="terms" required>
I agree to the <a href="/terms">Terms of Service</a>
</label>
</div>
<button type="submit">Create Account</button>
</form>
Key details:
autocompleteattributes help password managers and browser autofillaria-describedbylinks each input to its hint text — screen readers read the hint after the labelnovalidateon the form disables browser validation UI (when using custom JavaScript validation instead)- Radio buttons are grouped in a
fieldsetwithlegend - The checkbox label includes the link text, so it reads naturally: "I agree to the Terms of Service"
| What developers do | What they should do |
|---|---|
| Using placeholder text instead of visible labels Placeholders vanish when typing, leaving users with no field identification. They also have insufficient contrast in most browsers and are not reliably read by all screen readers | Always use a visible label element. Placeholders disappear on focus and lack accessibility |
| Using type='text' for everything Specific input types trigger specialized mobile keyboards and free built-in validation. type='text' gives neither | Use specific types: email, tel, url, number, date — they provide correct keyboards and built-in validation |
| Missing name attributes on form inputs The name attribute determines the key in the submitted data. Without it, the input's value is not included in the form submission | Every submittable input needs a name attribute |
| Forgetting autocomplete attributes autocomplete helps browsers and password managers fill forms correctly. It dramatically improves UX and conversion rates — especially on mobile | Add autocomplete='email', autocomplete='new-password', etc. |
Challenge: Build an Accessible Contact Form
Create a contact form with: name (required), email (required), subject dropdown (with 3 options), message textarea (required, 20-500 characters), and a file attachment (images only, max 5MB). Make it fully accessible.
Show Answer
<form action="/api/contact" method="post" enctype="multipart/form-data">
<h2>Contact Us</h2>
<div>
<label for="contact-name">Full name</label>
<input
type="text"
id="contact-name"
name="name"
required
autocomplete="name"
>
</div>
<div>
<label for="contact-email">Email address</label>
<input
type="email"
id="contact-email"
name="email"
required
autocomplete="email"
>
</div>
<div>
<label for="contact-subject">Subject</label>
<select id="contact-subject" name="subject" required>
<option value="">Choose a subject</option>
<option value="general">General Inquiry</option>
<option value="support">Technical Support</option>
<option value="billing">Billing Question</option>
</select>
</div>
<div>
<label for="contact-message">Message</label>
<textarea
id="contact-message"
name="message"
rows="6"
required
minlength="20"
maxlength="500"
aria-describedby="message-hint"
></textarea>
<p id="message-hint">Between 20 and 500 characters.</p>
</div>
<div>
<label for="contact-file">Attachment (optional)</label>
<input
type="file"
id="contact-file"
name="attachment"
accept="image/*"
aria-describedby="file-hint"
>
<p id="file-hint">Images only. Maximum 5MB.</p>
</div>
<button type="submit">Send Message</button>
</form>Key points:
enctype="multipart/form-data"is required for file uploads- Every input has a
labelwith matchingfor/id aria-describedbyconnects help text to inputsaccept="image/*"filters the file picker to images- Select has an empty-value placeholder option for "required" validation
autocompleteon name and email for faster filling
- 1Every input needs a visible label — placeholders are not labels and disappear when typing
- 2Use specific input types (email, tel, number, date) for mobile keyboards and free validation
- 3Every submittable input needs a name attribute — without it, the value is not sent
- 4Group related fields with fieldset/legend — critical for radio buttons and checkboxes
- 5Use aria-describedby to connect hint text and error messages to their inputs