Forms, validation and user input
Read and set values, use the constraint validation API with your own messages, collect data with FormData, and manage focus and selection.
Reading and setting values
const form = document.querySelector('#signup');
// every control, in document order
for (const field of form.elements) {
console.log(field.name, field.type, field.value);
}
// by name - the fastest way to a known field
const email = form.elements.namedItem('email');
email.value = '[email protected]';
// checkboxes and radios
const terms = form.elements.namedItem('terms');
terms.checked = true;
const plan = form.querySelector('input[name="plan"]:checked');
plan && plan.value;
// a multi-select
const chosen = [...form.elements.namedItem('tags').selectedOptions]
.map((o) => o.value);
// all values at once, including file inputs
const data = new FormData(form);
Object.fromEntries(data); // { email: "...", plan: "annual" }
data.getAll('tags'); // repeated names come back as a list
// an untracked form: build the payload without the DOM
const payload = Object.fromEntries(
[...form.elements].filter((f) => f.name).map((f) => [f.name, f.value])
);form.elementsis a live collection and includes buttons and fieldset contents.FormDatahonours disabled controls by skipping them and collects repeated names into a list, which is exactly the shape most servers expect.- Reading
.valueon a checkbox gives"on"by default - read.checkedinstead. - Values are strings. A number input still returns
"42"; convert explicitly withNumber()and check forNaN.
The constraint validation API
<form id="signup" novalidate>
<label for="email">Email</label>
<input id="email" name="email" type="email" required
minlength="6" autocomplete="email">
<span class="error" aria-live="polite"></span>
<label for="age">Age</label>
<input id="age" name="age" type="number" min="18" max="120">
<button type="submit">Sign up</button>
</form>const form = document.querySelector('#signup');
const email = form.elements.namedItem('email');
email.validity.valid; // the one flag most checks need
email.validity.valueMissing; // required and empty
email.validity.typeMismatch; // not a valid email address
email.validity.patternMismatch; // failed the pattern attribute
email.validity.tooShort; // below minlength
email.validity.rangeUnderflow; // below min
email.validity.badInput; // the browser could not parse what was typed
email.validity.customError; // set by you
email.checkValidity(); // fires an invalid event, returns a boolean
email.reportValidity(); // also shows the browser's message
form.checkValidity(); // does every field pass?
// your own rule, integrated with the browser's
function validateEmailField() {
const domain = email.value.split('@')[1] ?? '';
if (domain.endsWith('.invalid')) {
email.setCustomValidity('That domain cannot receive mail.');
} else {
email.setCustomValidity(''); // always clear it on the success path
}
}
email.addEventListener('input', validateEmailField);⚠️
A field with a custom error set stays invalid forever until
setCustomValidity("") is called. Forgetting the success branch is the classic bug: one bad value and the form can never be submitted again, with no visible reason.Submitting, resetting and focus
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
await save(new FormData(form));
});
// requestSubmit runs validation and fires the submit event
form.requestSubmit();
form.requestSubmit(otherButton); // acts as if that button was pressed
// reset restores default values and fires a reset event
form.addEventListener('reset', (event) => {
if (!confirm('Discard your changes?')) event.preventDefault();
});
form.reset();
// focus and text selection
email.focus({ preventScroll: true }); // focus without moving the page
email.setSelectionRange(0, email.value.length);
email.select();
email.selectionStart; // caret position
message.setRangeText('suffix', 3, 6, 'end');// a draft that survives a reload, without a framework
form.addEventListener('input', () => {
sessionStorage.setItem('signup:draft', JSON.stringify(Object.fromEntries(new FormData(form))));
});
const draft = sessionStorage.getItem('signup:draft');
if (draft) {
const values = JSON.parse(draft);
for (const [name, value] of Object.entries(values)) {
const field = form.elements.namedItem(name);
if (field) field.value = value;
}
}| Method | Validates? | Fires submit? |
|---|---|---|
form.submit() | No | No |
form.requestSubmit() | Yes | Yes |
| A real button click | Yes | Yes |
form.checkValidity() | Yes | No |
Use requestSubmit(). It is the only programmatic path that behaves like a user pressing the button, and therefore the only one that runs the validation and the listeners your code depends on.
FAQ
Should I use novalidate?
Only when you render your own error messages. Without it the browser shows its own bubble, which cannot be styled, cannot be localised by you, and disappears when focus moves. Adding
novalidate and calling reportValidity() yourself gives you full control while keeping the browser's rules.Why is my submit handler not running?
Most often the button has
type="button", which does not submit, or a listener calls preventDefault() on an ancestor. Also check that the handler is attached to the form and not to a container that was replaced by a re-render.Related
Events and delegation in depth Accessibility for DOM scripting
Last refreshed 2026-09-18.