HTML Forms
HTML forms are the primary mechanism for collecting user input and submitting it to a server for processing. A form wraps one or more form controls — inputs, buttons, selects, textareas — and defines how, where, and with what constraints the data is sent.
The <form> element acts as the container. Its attributes control submission behavior: action defines the endpoint URL, method selects GET or POST, enctype specifies the encoding for file uploads, and novalidate disables built-in validation.
| Attribute | Values | Description |
|---|---|---|
| action | URL | URL where form data is sent on submission |
| method | GET | POST | HTTP method for the request |
| enctype | application/x-www-form-urlencoded | multipart/form-data | text/plain | MIME type for encoding form data |
| novalidate | boolean | Disables browser validation on submit |
| target | _self | _blank | _parent | _top | Where to display the response |
| autocomplete | on | off | Browser autofill behavior |
| name | string | Identifies the form in DOM |
| Feature | GET | POST |
|---|---|---|
| Data visibility | Appended to URL (query string) | Sent in request body (hidden) |
| Bookmarkable | Yes — URL contains all data | No |
| Data size limit | ~2KB (URL length limit) | No practical limit |
| Security | Not for sensitive data (visible in URL) | Data in body — use HTTPS |
| File upload | Not supported | Supported (multipart/form-data) |
| Idempotent | Yes — safe for repeated requests | No — may create side effects |
| Use case | Search, filters, pagination | Login, registration, file upload |
| 1 | <!-- GET form — data in URL query string --> |
| 2 | <form action="/search" method="GET"> |
| 3 | <label for="q">Search:</label> |
| 4 | <input type="search" id="q" name="q" /> |
| 5 | <button type="submit">Search</button> |
| 6 | </form> |
| 7 | |
| 8 | <!-- POST form — data in request body --> |
| 9 | <form action="/signup" method="POST" enctype="application/x-www-form-urlencoded"> |
| 10 | <label for="email">Email:</label> |
| 11 | <input type="email" id="email" name="email" required /> |
| 12 | <button type="submit">Create Account</button> |
| 13 | </form> |
| 14 | |
| 15 | <!-- File upload — requires multipart/form-data --> |
| 16 | <form action="/upload" method="POST" enctype="multipart/form-data"> |
| 17 | <label for="file">Choose file:</label> |
| 18 | <input type="file" id="file" name="file" /> |
| 19 | <button type="submit">Upload</button> |
| 20 | </form> |
| 21 | |
| 22 | <!-- No validation — novalidate attribute --> |
| 23 | <form action="/submit" method="POST" novalidate> |
| 24 | <input type="email" name="email" /> |
| 25 | <button type="submit">Submit (no validation)</button> |
| 26 | </form> |
best practice
HTML5 defines over twenty input types, each optimized for a specific kind of data. Browsers render native controls — date pickers, color pickers, sliders — without JavaScript. The <input> element is self-closing and uses the type attribute to determine its behavior and appearance.
| Type | HTML | Description |
|---|---|---|
| text | <input type="text"> | Single-line text input (default) |
| <input type="email"> | Email address with built-in format validation | |
| password | <input type="password"> | Masked text entry |
| number | <input type="number"> | Numeric input with step/range controls |
| tel | <input type="tel"> | Telephone number (no validation pattern enforced) |
| url | <input type="url"> | URL with built-in format validation |
| search | <input type="search"> | Search field with clear button |
| date | <input type="date"> | Date picker (YYYY-MM-DD) |
| time | <input type="time"> | Time picker (HH:MM) |
| datetime-local | <input type="datetime-local"> | Date + time picker (no timezone) |
| month | <input type="month"> | Month and year picker |
| week | <input type="week"> | Week and year picker |
| color | <input type="color"> | Color picker (hexadecimal value) |
| range | <input type="range"> | Slider control (min, max, step, value) |
| file | <input type="file"> | File picker (accept, multiple, capture) |
| checkbox | <input type="checkbox"> | Boolean toggle (multiple allowed) |
| radio | <input type="radio"> | Single-select from group (same name) |
| hidden | <input type="hidden"> | Invisible field for passing data |
| image | <input type="image"> | Image as submit button (src, alt) |
| submit | <input type="submit"> | Submit button (value sets label) |
| reset | <input type="reset"> | Resets form to initial values |
| button | <input type="button"> | Clickable button (no default behavior) |
Live preview of various input types rendered in a form layout:
Text inputs and textareas are the most common form controls. The <input type="text"> handles single-line input, while <textarea> supports multi-line text entry with configurable dimensions.
| Attribute | text | textarea | Description |
|---|---|---|---|
| placeholder | ✓ | ✓ | Hint text shown before input |
| maxlength | ✓ | ✓ | Maximum character count |
| minlength | ✓ | ✓ | Minimum character count |
| pattern | ✓ | ✗ | Regex validation |
| readonly | ✓ | ✓ | Not editable, but submitted |
| disabled | ✓ | ✓ | Not editable, not submitted, grayed |
| spellcheck | ✓ | ✓ | Enable/disable spell checking |
| autocomplete | ✓ | ✓ | Browser autofill behavior |
| rows | ✗ | ✓ | Visible textarea height (lines) |
| cols | ✗ | ✓ | Visible textarea width (chars) |
| wrap | ✗ | ✓ | Line wrapping (soft|hard|off) |
| 1 | <!-- Single-line text input with validation --> |
| 2 | <input |
| 3 | type="text" |
| 4 | id="username" |
| 5 | name="username" |
| 6 | placeholder="johndoe" |
| 7 | minlength="3" |
| 8 | maxlength="20" |
| 9 | pattern="[a-zA-Z0-9_]+" |
| 10 | required |
| 11 | autocomplete="username" |
| 12 | /> |
| 13 | |
| 14 | <!-- Multi-line textarea --> |
| 15 | <textarea |
| 16 | id="bio" |
| 17 | name="bio" |
| 18 | rows="6" |
| 19 | cols="50" |
| 20 | maxlength="500" |
| 21 | placeholder="Tell us about yourself..." |
| 22 | wrap="soft" |
| 23 | ></textarea> |
| 24 | |
| 25 | <!-- Readonly and disabled examples --> |
| 26 | <input type="text" value="Pre-filled, not editable" readonly /> |
| 27 | <input type="text" value="Grayed out, not submitted" disabled /> |
| 28 | |
| 29 | <!-- Email with custom pattern --> |
| 30 | <input |
| 31 | type="email" |
| 32 | id="work-email" |
| 33 | name="work-email" |
| 34 | placeholder="name@company.com" |
| 35 | pattern="[a-z0-9._%+\-]+@[a-z0-9.\-]+.[a-z]{2,}$" |
| 36 | title="Enter a valid email address" |
| 37 | /> |
pro tip
Live preview of text inputs with various states:
Checkboxes allow multiple independent selections. Radio buttons restrict the user to exactly one choice within a group. Both use the checked attribute for preselection and the name attribute for grouping — radio buttons with the same name form a single group where only one can be selected.
| 1 | <!-- Checkboxes — independent choices --> |
| 2 | <fieldset> |
| 3 | <legend>Interests (select all that apply)</legend> |
| 4 | |
| 5 | <label> |
| 6 | <input type="checkbox" name="interests" value="coding" checked /> |
| 7 | Coding |
| 8 | </label> |
| 9 | <label> |
| 10 | <input type="checkbox" name="interests" value="design" /> |
| 11 | Design |
| 12 | </label> |
| 13 | <label> |
| 14 | <input type="checkbox" name="interests" value="writing" /> |
| 15 | Writing |
| 16 | </label> |
| 17 | </fieldset> |
| 18 | |
| 19 | <!-- Radio buttons — single choice --> |
| 20 | <fieldset> |
| 21 | <legend>Preferred Contact Method</legend> |
| 22 | |
| 23 | <label> |
| 24 | <input type="radio" name="contact" value="email" checked /> |
| 25 | |
| 26 | </label> |
| 27 | <label> |
| 28 | <input type="radio" name="contact" value="phone" /> |
| 29 | Phone |
| 30 | </label> |
| 31 | <label> |
| 32 | <input type="radio" name="contact" value="mail" /> |
| 33 | Postal Mail |
| 34 | </label> |
| 35 | </fieldset> |
| 36 | |
| 37 | <!-- Inline checkbox with explicit label --> |
| 38 | <input type="checkbox" id="terms" name="terms" required /> |
| 39 | <label for="terms"> |
| 40 | I agree to the <a href="/terms">Terms of Service</a> |
| 41 | </label> |
best practice
Live preview of checkboxes and radio groups:
File inputs allow users to upload files from their local device. The <input type="file"> element opens the system file picker. Key attributes include accept for filtering file types, multiple for batch uploads, and capture for mobile camera access.
| Attribute | Example | Description |
|---|---|---|
| accept | accept="image/*,.pdf" | MIME types or file extensions allowed |
| multiple | multiple | Allow selecting multiple files |
| capture | capture="environment" | Direct camera/mic access (mobile) |
| required | required | Form cannot submit without a file |
| files | input.files | FileList API (read-only, JavaScript) |
| 1 | <!-- Single file upload — images only --> |
| 2 | <label for="avatar">Profile Picture:</label> |
| 3 | <input |
| 4 | type="file" |
| 5 | id="avatar" |
| 6 | name="avatar" |
| 7 | accept="image/png,image/jpeg,image/webp" |
| 8 | /> |
| 9 | |
| 10 | <!-- Multiple file upload — PDFs and documents --> |
| 11 | <label for="documents">Upload Documents:</label> |
| 12 | <input |
| 13 | type="file" |
| 14 | id="documents" |
| 15 | name="documents" |
| 16 | multiple |
| 17 | accept=".pdf,.doc,.docx" |
| 18 | /> |
| 19 | |
| 20 | <!-- Mobile camera capture --> |
| 21 | <label for="photo">Take a Photo:</label> |
| 22 | <input |
| 23 | type="file" |
| 24 | id="photo" |
| 25 | name="photo" |
| 26 | accept="image/*" |
| 27 | capture="environment" |
| 28 | /> |
| 29 | |
| 30 | <!-- JavaScript: File API preview --> |
| 31 | <script> |
| 32 | const input = document.getElementById('avatar'); |
| 33 | const preview = document.getElementById('preview'); |
| 34 | |
| 35 | input.addEventListener('change', () => { |
| 36 | const file = input.files[0]; |
| 37 | if (file) { |
| 38 | const reader = new FileReader(); |
| 39 | reader.onload = (e) => { |
| 40 | preview.src = e.target.result; |
| 41 | }; |
| 42 | reader.readAsDataURL(file); |
| 43 | } |
| 44 | }); |
| 45 | </script> |
| 46 | <img id="preview" alt="Avatar preview" /> |
warning
HTML5 provides built-in constraint validation without JavaScript. Validation attributes include required, min/max, minlength/maxlength, pattern, step, and type-specific rules (email, url). The Constraint Validation API gives JavaScript access to validity states.
| Attribute | Applies To | Description |
|---|---|---|
| required | Most inputs | Field must have a value |
| min / max | number, date, range | Numeric or date range constraints |
| minlength / maxlength | text, textarea | Character count limits |
| pattern | text, tel, email, url | Regex pattern validation |
| step | number, date, range | Increment granularity |
| multiple | email, file | Comma-separated values (email) or multiple files |
The Constraint Validation API exposes the validityState object with boolean properties: valueMissing, typeMismatch, patternMismatch, tooLong, tooShort, rangeUnderflow, rangeOverflow, stepMismatch, badInput, and customError. Use setCustomValidity() to programmatically set validation messages.
| 1 | <!-- Validation attributes in action --> |
| 2 | <form id="signupForm" novalidate> |
| 3 | <label for="fullname">Full Name *</label> |
| 4 | <input |
| 5 | type="text" |
| 6 | id="fullname" |
| 7 | name="fullname" |
| 8 | required |
| 9 | minlength="2" |
| 10 | maxlength="100" |
| 11 | pattern="[A-Za-z\s]+" |
| 12 | title="Only letters and spaces allowed" |
| 13 | /> |
| 14 | |
| 15 | <label for="age">Age *</label> |
| 16 | <input |
| 17 | type="number" |
| 18 | id="age" |
| 19 | name="age" |
| 20 | required |
| 21 | min="18" |
| 22 | max="120" |
| 23 | step="1" |
| 24 | /> |
| 25 | |
| 26 | <label for="postcode">Postal Code</label> |
| 27 | <input |
| 28 | type="text" |
| 29 | id="postcode" |
| 30 | name="postcode" |
| 31 | pattern="[A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][ABD-HJLNP-UW-Z]{2}" |
| 32 | title="UK postcode format e.g. SW1A 1AA" |
| 33 | /> |
| 34 | |
| 35 | <button type="submit">Submit</button> |
| 36 | </form> |
| 37 | |
| 38 | <script> |
| 39 | const form = document.getElementById('signupForm'); |
| 40 | const fullname = document.getElementById('fullname'); |
| 41 | |
| 42 | form.addEventListener('submit', (e) => { |
| 43 | e.preventDefault(); |
| 44 | |
| 45 | // Check validity |
| 46 | if (!fullname.checkValidity()) { |
| 47 | const state = fullname.validity; |
| 48 | if (state.valueMissing) { |
| 49 | fullname.setCustomValidity('Name is required'); |
| 50 | } else if (state.patternMismatch) { |
| 51 | fullname.setCustomValidity('Only letters and spaces'); |
| 52 | } else if (state.tooShort) { |
| 53 | fullname.setCustomValidity('Name must be at least 2 characters'); |
| 54 | } |
| 55 | fullname.reportValidity(); |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | // Reset custom validity and submit |
| 60 | fullname.setCustomValidity(''); |
| 61 | this.submit(); |
| 62 | }); |
| 63 | </script> |
info
CSS pseudo-classes enable styling form controls based on their validation state, without JavaScript. These classes are applied automatically by the browser as the user interacts with the form.
| Pseudo-class | Applied When |
|---|---|
| :valid | Field passes all validation constraints |
| :invalid | Field fails any validation constraint |
| :required | Field has the required attribute |
| :optional | Field does not have required |
| :in-range | Value is within min/max bounds |
| :out-of-range | Value is outside min/max bounds |
| :user-valid | User has interacted and value is valid |
| :user-invalid | User has interacted and value is invalid |
| :placeholder-shown | Placeholder is currently visible |
| :focus-within | Element or any descendant has focus |
| 1 | /* Input base styles */ |
| 2 | input { |
| 3 | border: 1px solid #ccc; |
| 4 | padding: 8px 12px; |
| 5 | transition: border-color 0.2s, box-shadow 0.2s; |
| 6 | } |
| 7 | |
| 8 | /* Valid state — green border */ |
| 9 | input:valid { |
| 10 | border-color: #22c55e; |
| 11 | } |
| 12 | |
| 13 | /* Invalid state — red border */ |
| 14 | input:invalid { |
| 15 | border-color: #ef4444; |
| 16 | } |
| 17 | |
| 18 | /* User-interacted invalid — show error styling */ |
| 19 | input:user-invalid { |
| 20 | border-color: #ef4444; |
| 21 | box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15); |
| 22 | } |
| 23 | |
| 24 | /* User-interacted valid — confirm styling */ |
| 25 | input:user-valid { |
| 26 | border-color: #22c55e; |
| 27 | box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15); |
| 28 | } |
| 29 | |
| 30 | /* Required field indicator */ |
| 31 | input:required + label::after { |
| 32 | content: " *"; |
| 33 | color: #ef4444; |
| 34 | } |
| 35 | |
| 36 | /* Range slider states */ |
| 37 | input[type="range"]:in-range { |
| 38 | background: #22c55e; |
| 39 | } |
| 40 | |
| 41 | input[type="range"]:out-of-range { |
| 42 | background: #ef4444; |
| 43 | } |
| 44 | |
| 45 | /* Focus styles */ |
| 46 | input:focus { |
| 47 | outline: none; |
| 48 | border-color: #3b82f6; |
| 49 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); |
| 50 | } |
| 51 | |
| 52 | /* Disabled state */ |
| 53 | input:disabled { |
| 54 | opacity: 0.5; |
| 55 | cursor: not-allowed; |
| 56 | background: #f5f5f5; |
| 57 | } |
best practice
Live preview of form styling with validation states:
Accessible forms ensure all users — including those using screen readers, keyboard navigation, or assistive technologies — can successfully interact with your interface. The foundation is proper labeling, logical grouping, and clear error communication.
| Technique | Implementation | Purpose |
|---|---|---|
| Explicit label | <label for="id"> | Associates label with input (clickable, screen reader) |
| aria-label | aria-label="Search" | Invisible label when visual label is not possible |
| aria-labelledby | aria-labelledby="desc-id" | References another element as label |
| fieldset + legend | <fieldset><legend> | Groups related controls with description |
| aria-invalid | aria-invalid="true" | Indicates invalid field to screen readers |
| aria-describedby | aria-describedby="error-msg" | Links input to error or help text |
| aria-required | aria-required="true" | Announces field as required |
| role="alert" | role="alert" on error div | Immediately announces error to screen reader |
| 1 | <!-- Properly labeled form with error handling --> |
| 2 | <form novalidate id="signupForm"> |
| 3 | <fieldset> |
| 4 | <legend>Personal Information</legend> |
| 5 | |
| 6 | <label for="fullname">Full Name</label> |
| 7 | <input |
| 8 | type="text" |
| 9 | id="fullname" |
| 10 | name="fullname" |
| 11 | required |
| 12 | aria-required="true" |
| 13 | aria-describedby="name-hint name-error" |
| 14 | /> |
| 15 | <span id="name-hint" class="hint"> |
| 16 | Enter your full name (minimum 2 characters) |
| 17 | </span> |
| 18 | <span id="name-error" class="error" role="alert" aria-live="assertive"></span> |
| 19 | |
| 20 | <label for="email">Email Address</label> |
| 21 | <input |
| 22 | type="email" |
| 23 | id="email" |
| 24 | name="email" |
| 25 | required |
| 26 | aria-required="true" |
| 27 | aria-describedby="email-error" |
| 28 | /> |
| 29 | <span id="email-error" class="error" role="alert" aria-live="polite"></span> |
| 30 | </fieldset> |
| 31 | |
| 32 | <fieldset> |
| 33 | <legend>Payment Method</legend> |
| 34 | |
| 35 | <label> |
| 36 | <input type="radio" name="payment" value="cc" checked /> |
| 37 | Credit Card |
| 38 | </label> |
| 39 | <label> |
| 40 | <input type="radio" name="payment" value="paypal" /> |
| 41 | PayPal |
| 42 | </label> |
| 43 | </fieldset> |
| 44 | |
| 45 | <button type="submit">Sign Up</button> |
| 46 | </form> |
| 47 | |
| 48 | <script> |
| 49 | const form = document.getElementById('signupForm'); |
| 50 | |
| 51 | form.addEventListener('submit', (e) => { |
| 52 | e.preventDefault(); |
| 53 | let hasError = false; |
| 54 | |
| 55 | const fields = [ |
| 56 | { id: 'fullname', errorId: 'name-error', message: 'Full name is required' }, |
| 57 | { id: 'email', errorId: 'email-error', message: 'Valid email is required' }, |
| 58 | ]; |
| 59 | |
| 60 | fields.forEach(({ id, errorId, message }) => { |
| 61 | const input = document.getElementById(id); |
| 62 | const errorEl = document.getElementById(errorId); |
| 63 | |
| 64 | if (!input.validity.valid) { |
| 65 | errorEl.textContent = message; |
| 66 | input.setAttribute('aria-invalid', 'true'); |
| 67 | hasError = true; |
| 68 | } else { |
| 69 | errorEl.textContent = ''; |
| 70 | input.removeAttribute('aria-invalid'); |
| 71 | } |
| 72 | }); |
| 73 | |
| 74 | if (!hasError) { |
| 75 | alert('Form submitted successfully!'); |
| 76 | } |
| 77 | }); |
| 78 | </script> |
best practice
Security
Forms are a primary attack vector. Protecting against Cross-Site Request Forgery (CSRF) and Cross-Site Scripting (XSS) is essential for any production application.
| Threat | Risk | Mitigation |
|---|---|---|
| CSRF | Attacker submits form on user's behalf | CSRF tokens, SameSite cookies, origin checks |
| XSS | Malicious script injected into output | Output encoding, Content-Security-Policy |
| SQL Injection | Database compromise via form input | Parameterized queries, ORMs, input sanitization |
| File Upload Abuse | Malicious files uploaded to server | Validate MIME, scan files, restrict extensions |
| Clickjacking | User tricked into clicking hidden form | X-Frame-Options header, CSP frame-ancestors |
UX Patterns
pro tip
Live preview of a mobile-friendly, well-structured form incorporating accessibility and UX best practices: