|$ curl https://forge-ai.dev/api/markdown?path=docs/html/forms-api
$cat docs/forms-api.md
updated Last week·22 min read·published

Forms API

HTMLFormsIntermediateIntermediate🎯Free Tools
Introduction

The Forms API is a collection of JavaScript interfaces and HTML attributes that make forms interactive, accessible, and easy to validate. It includes the FormData API for reading and manipulating form data, the Constraint Validation API for programmatic validation, and a rich set of form-related events.

Understanding the Forms API lets you build forms that work without JavaScript (progressive enhancement), provide rich client-side validation, and integrate cleanly with server-side processing. This guide covers every major interface with practical examples.

FormData API

The FormData interface provides a way to construct a set of key-value pairs representing form fields and their values, which can be sent using fetch() or XMLHttpRequest. It automatically handles encoding using the form's enctype attribute:

formdata-basic.html
HTML
1<form id="registration-form">
2 <input type="text" name="username" required>
3 <input type="email" name="email" required>
4 <input type="password" name="password" required>
5 <select name="role">
6 <option value="user">User</option>
7 <option value="admin">Admin</option>
8 </select>
9 <input type="file" name="avatar" accept="image/*">
10 <button type="submit">Register</button>
11</form>
12
13<script>
14 const form = document.getElementById('registration-form');
15
16 form.addEventListener('submit', async (event) => {
17 event.preventDefault();
18
19 // Create FormData from the form element
20 const formData = new FormData(form);
21
22 // Log all entries
23 for (const [key, value] of formData.entries()) {
24 console.log(key + ':', value);
25 }
26
27 // FormData is automatically encoded as multipart/form-data
28 // when sent via fetch
29 const response = await fetch('/api/register', {
30 method: 'POST',
31 body: formData,
32 // Do NOT set Content-Type header -- the browser sets it
33 // automatically with the correct boundary
34 });
35
36 const result = await response.json();
37 console.log('Registration:', result);
38 });
39</script>

FormData provides several methods for programmatic manipulation:

formdata-methods.js
JavaScript
1// Creating FormData in different ways
2
3// From a form element
4const form = document.querySelector('form');
5const fd1 = new FormData(form);
6
7// From scratch (no form needed)
8const fd2 = new FormData();
9fd2.append('name', 'Jane');
10fd2.append('age', '30');
11fd2.append('interests', 'HTML');
12fd2.append('interests', 'CSS'); // multiple values for same key
13
14// Methods available on FormData
15fd2.append('key', 'value'); // Add a new field
16fd2.set('key', 'value'); // Set field (replaces existing)
17fd2.delete('key'); // Remove a field
18fd2.has('key'); // Check if field exists
19fd2.get('key'); // Get first value for key
20fd2.getAll('key'); // Get all values for key
21
22// Iteration
23for (const [key, value] of fd2.entries()) {
24 console.log(key, value);
25}
26fd2.forEach((value, key) => {
27 console.log(key, value);
28});
29
30// Converting to URL-encoded string
31const params = new URLSearchParams(fd2);
32console.log(params.toString());
33// "name=Jane&age=30&interests=HTML&interests=CSS"
34
35// Converting to plain object
36const obj = Object.fromEntries(fd2.entries());
37console.log(obj);
38// { name: "Jane", age: "30", interests: "CSS" }
39// Note: only last value per key with Object.fromEntries

info

When using fetch() with FormData, do not manually set the Content-Type header. The browser automatically sets it to multipart/form-data with the correct boundary. Setting it manually will break the request.
FormData with Fetch

The most common use of FormData is sending form data via fetch. Here is a complete pattern including file uploads and error handling:

formdata-fetch.html
HTML
1<form id="upload-form" enctype="multipart/form-data">
2 <input type="text" name="title" required>
3 <textarea name="description"></textarea>
4 <input type="file" name="file" accept=".pdf,.doc,.docx" required>
5 <input type="hidden" name="csrf_token" value="abc123">
6 <button type="submit" id="submit-btn">Upload</button>
7 <div id="status"></div>
8</form>
9
10<script>
11 const form = document.getElementById('upload-form');
12 const status = document.getElementById('status');
13
14 form.addEventListener('submit', async (e) => {
15 e.preventDefault();
16 const btn = document.getElementById('submit-btn');
17 btn.disabled = true;
18 btn.textContent = 'Uploading...';
19
20 const formData = new FormData(form);
21
22 // Log form data contents
23 console.log('Files:', formData.getAll('file'));
24 console.log('Title:', formData.get('title'));
25
26 try {
27 const response = await fetch('/api/upload', {
28 method: 'POST',
29 body: formData,
30 });
31
32 if (!response.ok) {
33 const error = await response.json();
34 throw new Error(error.message || 'Upload failed');
35 }
36
37 const result = await response.json();
38 status.textContent = 'Upload successful: ' + result.url;
39 status.style.color = '#00FF41';
40 form.reset();
41 } catch (error) {
42 status.textContent = 'Error: ' + error.message;
43 status.style.color = '#FF3355';
44 } finally {
45 btn.disabled = false;
46 btn.textContent = 'Upload';
47 }
48 });
49</script>

best practice

Always validate file types and sizes both on the client (for UX) and on the server (for security). Client-side validation can be bypassed — never trust it as your only line of defense.
Constraint Validation API

The Constraint Validation API lets you check and customize form field validation programmatically. Every form element has a validity property that returns a ValidityState object with boolean flags for each possible constraint failure:

constraint-validation.html
HTML
1<form id="validated-form" novalidate>
2 <input type="email" name="email" id="email-input"
3 required minlength="5" maxlength="254"
4 placeholder="user@example.com">
5 <span id="email-error" class="error"></span>
6
7 <input type="url" name="website" id="website-input"
8 required pattern="https?://.+"
9 placeholder="https://example.com">
10 <span id="website-error" class="error"></span>
11
12 <input type="number" name="age" id="age-input"
13 required min="18" max="120">
14 <span id="age-error" class="error"></span>
15
16 <button type="submit">Submit</button>
17</form>
18
19<script>
20 const form = document.getElementById('validated-form');
21 const emailInput = document.getElementById('email-input');
22
23 // Check validity programmatically
24 console.log(emailInput.validity);
25 // ValidityState {
26 // valueMissing, typeMismatch, patternMismatch,
27 // tooLong, tooShort, rangeUnderflow, rangeOverflow,
28 // stepMismatch, badInput, customError, valid
29 // }
30
31 // Check if field is valid
32 console.log(emailInput.checkValidity()); // true or false
33
34 // Get validation message
35 console.log(emailInput.validationMessage); // "" or constraint msg
36
37 // Custom validation messages
38 emailInput.addEventListener('input', () => {
39 if (emailInput.validity.valueMissing) {
40 emailInput.setCustomValidity('Email address is required');
41 } else if (emailInput.validity.typeMismatch) {
42 emailInput.setCustomValidity('Please enter a valid email');
43 } else if (emailInput.validity.tooShort) {
44 emailInput.setCustomValidity('Email must be at least 5 characters');
45 } else {
46 emailInput.setCustomValidity(''); // Clear custom error
47 }
48 });
49
50 // Submit handler with full validation
51 form.addEventListener('submit', (e) => {
52 e.preventDefault();
53 const fields = form.querySelectorAll('input, textarea, select');
54 let isValid = true;
55
56 fields.forEach(field => {
57 if (!field.checkValidity()) {
58 isValid = false;
59 field.classList.add('invalid');
60 const errorEl = document.getElementById(field.id + '-error');
61 if (errorEl) errorEl.textContent = field.validationMessage;
62 } else {
63 field.classList.remove('invalid');
64 const errorEl = document.getElementById(field.id + '-error');
65 if (errorEl) errorEl.textContent = '';
66 }
67 });
68
69 if (isValid) form.submit();
70 });
71</script>
ValidityState Properties

The ValidityState object has boolean properties that describe exactly why a field failed validation:

PropertyDescriptionTriggered By
valueMissingField is required but emptyrequired attribute
typeMismatchValue does not match expected typetype="email" or type="url"
patternMismatchValue does not match the patternpattern attribute (regex)
tooLongValue exceeds maxlengthmaxlength attribute
tooShortValue is shorter than minlengthminlength attribute
rangeUnderflowNumber is below minimummin attribute
rangeOverflowNumber exceeds maximummax attribute
stepMismatchNumber does not align with stepstep attribute
badInputText entered in number/date fieldtype="number" etc.
customErrorCustom error via setCustomValiditysetCustomValidity()
validAll constraints satisfiedAll other properties are false
validity-state.js
JavaScript
1// Detailed validity checking
2const input = document.querySelector('input[type="email"]');
3
4function getValidationError(field) {
5 const v = field.validity;
6
7 if (v.valueMissing) return 'This field is required.';
8 if (v.typeMismatch) return 'Please enter a valid ' + field.type + '.';
9 if (v.patternMismatch) return 'Value does not match the required format.';
10 if (v.tooLong) return 'Maximum length is ' + field.maxLength + '.';
11 if (v.tooShort) return 'Minimum length is ' + field.minLength + '.';
12 if (v.rangeUnderflow) return 'Minimum value is ' + field.min + '.';
13 if (v.rangeOverflow) return 'Maximum value is ' + field.max + '.';
14 if (v.stepMismatch) return 'Value must be in steps of ' + field.step + '.';
15 if (v.badInput) return 'Please enter a valid value.';
16 if (v.customError) return field.validationMessage;
17
18 return null; // Field is valid
19}
20
21// Real-time validation on every input
22document.querySelectorAll('input, textarea').forEach(field => {
23 field.addEventListener('input', () => {
24 const error = getValidationError(field);
25 const errorEl = field.nextElementSibling;
26
27 if (error) {
28 field.setCustomValidity(error);
29 if (errorEl) errorEl.textContent = error;
30 field.setAttribute('aria-invalid', 'true');
31 } else {
32 field.setCustomValidity('');
33 if (errorEl) errorEl.textContent = '';
34 field.removeAttribute('aria-invalid');
35 }
36 });
37});
Custom Validation Messages

The setCustomValidity()method lets you override the browser's default error messages. Passing an empty string clears the custom error. This is useful for complex rules that cannot be expressed with HTML attributes alone:

custom-validation.html
HTML
1<form id="signup-form" novalidate>
2 <input type="password" id="password" name="password"
3 required minlength="8">
4 <span class="error" id="password-error"></span>
5
6 <input type="password" id="confirm-password"
7 name="confirm-password" required>
8 <span class="error" id="confirm-error"></span>
9
10 <input type="text" id="username" name="username"
11 required minlength="3" maxlength="20">
12 <span class="error" id="username-error"></span>
13
14 <button type="submit">Sign Up</button>
15</form>
16
17<script>
18 const form = document.getElementById('signup-form');
19 const password = document.getElementById('password');
20 const confirmPassword = document.getElementById('confirm-password');
21 const username = document.getElementById('username');
22
23 // Custom validation: passwords must match
24 confirmPassword.addEventListener('input', () => {
25 if (confirmPassword.value !== password.value) {
26 confirmPassword.setCustomValidity('Passwords do not match');
27 document.getElementById('confirm-error').textContent =
28 'Passwords do not match';
29 } else {
30 confirmPassword.setCustomValidity('');
31 document.getElementById('confirm-error').textContent = '';
32 }
33 });
34
35 // Custom validation: password strength
36 password.addEventListener('input', () => {
37 const value = password.value;
38 const hasUpper = /[A-Z]/.test(value);
39 const hasLower = /[a-z]/.test(value);
40 const hasNumber = /[0-9]/.test(value);
41 const hasSpecial = /[!@#$%^&*]/.test(value);
42
43 if (value.length < 8) {
44 password.setCustomValidity('Must be at least 8 characters');
45 } else if (!(hasUpper && hasLower)) {
46 password.setCustomValidity('Must contain upper and lowercase letters');
47 } else if (!hasNumber) {
48 password.setCustomValidity('Must contain at least one number');
49 } else if (!hasSpecial) {
50 password.setCustomValidity('Must contain a special character');
51 } else {
52 password.setCustomValidity('');
53 }
54 document.getElementById('password-error').textContent =
55 password.validationMessage;
56 });
57
58 // Custom validation: username format
59 username.addEventListener('input', () => {
60 if (/^[a-zA-Z0-9_]+$/.test(username.value) || username.value === '') {
61 username.setCustomValidity('');
62 } else {
63 username.setCustomValidity(
64 'Only letters, numbers, and underscores allowed'
65 );
66 }
67 document.getElementById('username-error').textContent =
68 username.validationMessage;
69 });
70
71 form.addEventListener('submit', (e) => {
72 e.preventDefault();
73 if (form.checkValidity()) {
74 console.log('Form is valid! Submitting...');
75 } else {
76 console.log('Form has validation errors.');
77 }
78 });
79</script>

warning

setCustomValidity()replaces ALL validation messages — even native ones like "required". When you set a custom error, the browser shows your message instead of the built-in one. Always clear the custom error by passing an empty string before checking native constraints.
Form Events

Forms fire several events that let you respond to user interactions. The key events are submit, input, change, invalid, and reset:

form-events.html
HTML
1<form id="event-demo" novalidate>
2 <input type="email" name="email" required>
3 <input type="text" name="username" required minlength="3">
4 <button type="submit">Submit</button>
5 <button type="reset">Reset</button>
6</form>
7
8<div id="event-log"></div>
9
10<script>
11 const form = document.getElementById('event-demo');
12 const log = document.getElementById('event-log');
13
14 function addLog(message) {
15 const entry = document.createElement('div');
16 entry.textContent = new Date().toLocaleTimeString()
17 + ': ' + message;
18 log.appendChild(entry);
19 }
20
21 // submit: fires when form is submitted
22 form.addEventListener('submit', (e) => {
23 e.preventDefault();
24 addLog('SUBMIT: Form submitted');
25 // Validate, then call form.submit() or send via fetch
26 });
27
28 // input: fires on every keystroke/change in any field
29 form.addEventListener('input', (e) => {
30 const field = e.target;
31 addLog('INPUT: ' + field.name + ' = "' + field.value + '"');
32 // Real-time validation
33 if (field.checkValidity()) {
34 field.style.borderColor = '#00FF41';
35 } else {
36 field.style.borderColor = '#FF3355';
37 }
38 });
39
40 // change: fires when field loses focus after value changed
41 form.addEventListener('change', (e) => {
42 addLog('CHANGE: ' + e.target.name + ' was updated');
43 });
44
45 // invalid: fires when field fails constraint validation
46 form.addEventListener('invalid', (e) => {
47 e.preventDefault(); // Prevent default browser tooltip
48 const field = e.target;
49 addLog('INVALID: ' + field.name
50 + ' -- ' + field.validationMessage);
51 field.style.borderColor = '#FF3355';
52 }, true); // Capture phase to catch all invalid events
53
54 // reset: fires when form is reset
55 form.addEventListener('reset', () => {
56 addLog('RESET: Form fields cleared');
57 form.querySelectorAll('input').forEach(f => {
58 f.style.borderColor = '';
59 });
60 });
61</script>
EventTargetWhen It Fires
submitformUser clicks submit or presses Enter
inputinput, textarea, selectEvery keystroke, paste, or value change
changeinput, textarea, selectField loses focus after value was changed
invalidform elementsField fails constraint validation
resetformUser clicks reset or calls form.reset()
focusin / focusoutform elementsField gains or loses focus (bubbles)
Progressive Enhancement

Progressive enhancement means forms work without JavaScript and become richer when JS is available. HTML5 validation attributes provide the baseline, while the Forms API layers on top for enhanced experiences:

progressive-enhancement.html
HTML
1<!-- Baseline: works without JavaScript -->
2<form action="/api/register" method="POST">
3 <input type="text" name="username" required minlength="3"
4 maxlength="20" pattern="[a-zA-Z0-9_]+"
5 placeholder="Username">
6
7 <input type="email" name="email" required
8 placeholder="user@example.com">
9
10 <input type="password" name="password" required minlength="8"
11 placeholder="Password (min 8 characters)">
12
13 <button type="submit">Register</button>
14</form>
15
16<!-- Enhancement: with JavaScript for richer UX -->
17<script>
18 const form = document.querySelector('form');
19
20 // Feature detection: check for Constraint Validation API
21 if ('validity' in document.createElement('input')) {
22 form.setAttribute('novalidate', '');
23
24 form.addEventListener('input', (e) => {
25 const field = e.target;
26 if (field.validity.valid) {
27 field.style.borderColor = '#00FF41';
28 } else {
29 field.style.borderColor = '#FF3355';
30 }
31 });
32
33 form.addEventListener('submit', async (e) => {
34 e.preventDefault();
35
36 if (!form.checkValidity()) {
37 const firstInvalid = form.querySelector(':invalid');
38 firstInvalid.focus();
39 return;
40 }
41
42 // Submit via fetch instead of page reload
43 const formData = new FormData(form);
44 try {
45 const response = await fetch('/api/register', {
46 method: 'POST',
47 body: formData,
48 });
49
50 if (response.ok) {
51 window.location.href = '/welcome';
52 } else {
53 const error = await response.json();
54 alert(error.message);
55 }
56 } catch {
57 // Fallback: submit traditionally
58 form.submit();
59 }
60 });
61 }
62 // Without Constraint Validation API,
63 // form submits normally via action URL
64</script>

best practice

Always provide a form that works without JavaScript — use HTML5 validation attributes and a valid action URL. Then enhance with JavaScript for real-time feedback and fetch submission. This ensures your form works for all users.
Live Demo

This demo demonstrates real-time validation and custom error messages in action:

preview
Best Practices
Use HTML5 validation attributes as the baseline — they work without JavaScript
Layer the Constraint Validation API on top for real-time feedback and custom messages
Always validate on the server — client-side validation is for UX, not security
Use FormData with fetch() for async form submission without page reloads
Never set Content-Type manually when using FormData with fetch
Use the invalid event with capture phase to prevent default browser tooltips
Provide clear, specific error messages that tell users exactly how to fix the issue
Use feature detection before relying on Constraint Validation API methods
Test forms with JavaScript disabled to ensure progressive enhancement works
Set aria-invalid and aria-describedby for accessible error announcements
🔥

pro tip

The Constraint Validation API is one of HTML5's most underused features. It eliminates the need for most validation libraries and gives you full control over error messages while maintaining progressive enhancement.
$ForgeLearn — Engineering Documentation·Section ID: HTML-18·Revision: 1.0