Forms — Inputs, Validation and Submission
IntermediateForms are where most user data enters an application. The browser gives you validation, keyboard handling and accessibility free — if you use a real form element rather than a div with a button.
Overview
It is tempting to build a form out of divs and handle everything in JavaScript, and it is almost always the wrong call. A real form element gives you Enter-to-submit, browser validation, autofill, and correct screen-reader behaviour with no work. What you add is the submit handler, which should call preventDefault and take over from there. FormData reads every named field in one line, which is far less brittle than reading each input by id.
Reading a Form
FormData collects every named field, including files. It is the reason forms need name attributes.
form.addEventListener('submit', async (e) => {
e.preventDefault() // stop the page navigating
const data = new FormData(form)
data.get('email') // by name attribute
Object.fromEntries(data) // plain object, for JSON APIs
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(data)),
})
})
// Sending files: pass the FormData directly, no Content-Type header
await fetch('/api/upload', { method: 'POST', body: new FormData(form) })
// Checkboxes and multi-selects need getAll
data.getAll('topics') // ['arrays', 'graphs']Browser Validation
Constraint attributes give you validation and messaging for free. You can style the states and override the messages.
<input type="email" name="email" required />
<input type="password" name="pw" minlength="8" required />
<input type="number" name="qty" min="1" max="10" />
<input type="text" name="pin" pattern="[0-9]{6}" />
// Checking from JavaScript
input.checkValidity() // boolean
form.reportValidity() // shows the browser's messages
input.validity.tooShort // which rule failed
// Custom message
input.setCustomValidity(
input.validity.patternMismatch ? 'PIN must be 6 digits' : ''
)
// Styling states
input:invalid { border-color: red }
input:user-invalid { ... } // only after the user has interactedControlled Inputs
The pattern React uses: state is the source of truth, the input reflects it, and every keystroke updates state.
// Vanilla equivalent of a controlled input
let value = ''
input.value = value
input.addEventListener('input', (e) => {
value = e.target.value // state updates
render() // UI follows state
})
// Server validation errors, mapped back to fields
const res = await fetch('/api/signup', { ... })
if (res.status === 422) {
const { errors } = await res.json()
for (const [field, message] of Object.entries(errors)) {
form.elements[field].setCustomValidity(message)
}
form.reportValidity()
}
// Always disable the submit button while in flight,
// or an impatient double-click creates two accounts.Key Points to Remember
- 1A real form element gives Enter-to-submit, validation, autofill and screen-reader support for free
- 2preventDefault in the submit handler stops the page navigating away
- 3FormData reads every named field at once — which is why inputs need name attributes, not just ids
- 4Constraint attributes (required, minlength, pattern) provide validation without JavaScript; :user-invalid styles only after interaction
- 5Disable the submit button while the request is in flight, or a double-click submits twice
Interview Questions
Sign in to ask AriaWhy use a <form> element instead of a div with a submit button?
How do you read all values from a form in one step?
How would you map server-side validation errors back onto individual form fields?
Ask Aria about Forms — Inputs, Validation and Submission
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.