Form events
React to form typing, committed changes, safe submission, clipboard actions, and focus movement without surprising the user or navigating away.
- 01Pick the right eventUse input to update while the person types, and change to act once the value is final.
- 02Handle a submit safelyKeep the page from reloading, find the button with event.submitter, and know how requestSubmit and submit differ.
- 03Work with paste and focusClean pasted text, add to copied text, and check a field when the person leaves it.
Forms send events
A person fills in a form in small steps. They type. They paste. They pick an option. They move to the next field. At the end, they send the form. Your JavaScript can react to each of these steps.
The browser tells your code about each step with an event: a short message that says "this just happened". You met events in Introduction to events. This lesson covers the events that forms send:
input: the value just changed, while the person is still editing.change: the person has finished changing the value.submit: the person is sending the whole form.cut,copy, andpaste: the person used the clipboard, the place where your computer keeps copied text.- Focus events: the person moved into a field or out of it.
A form event is a message from the browser about what a person did in a form: typing, choosing, sending, using the clipboard, or moving between fields.
One rule matters a lot. These events report what the person does. Typing, pasting, voice typing, picking an option, choosing a file, ticking a checkbox, and the browser's autofill all count. When your own code sets a value, such as input.value = "Ada", no input or change event happens.
input or change
INTERACTIVEinput is the live event. It runs every time the person changes the value. Typing a letter, pasting, deleting, undo, voice typing, and phone keyboard suggestions all count. Use input for things that should update while the person types, such as a character counter, a live preview, or a search filter.
change is the "done" event. When it runs depends on the kind of field:
- A text field, or a textarea (a box for several lines of text):
changeruns when the person leaves the field after changing it. Sometimes pressing Enter counts too. - A checkbox, a radio button, a select menu, a date picker, or a file picker:
changeruns right away, because picking is already the final choice.
A sports commentator talks during the whole game. They tell you every pass and every shot as it happens. The next morning, the newspaper prints only the final score. input is the commentator. change is the newspaper.
- In real life: The commentator talks during the game
- In JavaScript:
inputruns on every change the person makes - In real life: The newspaper prints the final score
- In JavaScript:
changeruns once the value is final - In real life: Every pass and every shot
- In JavaScript: typing, pasting, deleting, voice typing
- In real life: The end of the game
- In JavaScript: leaving the field, or picking an option
const controls = form.elements;form.addEventListener("input", logEvent);form.addEventListener("change", logEvent); function logEvent(event) { const target = event.target; const value = target.type === "checkbox" ? target.checked : target.value; addRow(event.type, target.name, value, event.inputType ?? "—");}- No events yet.
Use any field. A text field sends input on every letter. change waits until you leave it.
input and change listeners on the form write this log. When code clears the fields, no rows are added.Try the text field first. You get one input row for each letter you type. The change row waits until you press Tab or click somewhere else. Then try the checkbox and the menu. Each one gives its rows right away, because picking is already final.
For text edits, event.inputType says what kind of edit it was. For example: insertText for typing, insertFromPaste for pasting, or deleteContentBackward for Backspace.
The beforeinput event runs just before the edit happens. At that moment the value has not changed yet, so your code can still stop the edit. input runs after the edit, when the new value is already in the field. Both events have inputType for text edits.
const field = document.querySelector("#clipboard-field");
field.addEventListener("beforeinput", (event) => {
console.log("before:" + event.inputType);
});
field.addEventListener("input", (event) => {
console.log("after:" + event.inputType);
});Type one letter in this field. Line 3 prints before:insertText first. Then line 6 prints after:insertText. A text editor app can use beforeinput to block an edit. For counters and previews, plain input is simpler.
const events = [];
events.push("checkbox:input");
events.push("checkbox:change");
events.push("select:input");
events.push("select:change");
console.log(events.join(" -> "));This code writes down the order a browser usually uses. Ticking a checkbox gives input and then change, both at once. Picking from a select menu does the same. There is no "still typing" time in between, because the choice is already final.
Read each row from left to right: a question about your form, the event to use, and why.
| Your question | Use this event | Why |
|---|---|---|
| Do I need every change while the person types? | input | A counter, a preview, or a filter must show each new value. |
| Do I need only the final value? | change | Saving once when the person finishes a field is better than saving after every letter. |
| Is it a checkbox, a radio button, or a menu? | Either one | These fields finish right away, so both events run at the same time. |
Did my own code set .value? | Neither | Code changes send no input or change event. Update your own data directly instead. |
Step through a model of typing two letters and then pressing Tab. It is plain JavaScript that copies the order a real browser uses.
script
let value = "";value += "h";log.push("input:" + value);value += "i";log.push("input:" + value);log.push("change:" + value);log.push("focusout:" + value);console.log(log.join(" -> "));This step-through is a model: plain JavaScript that copies the order of the events, because it has no real form inside it. The order comes from a real test in Chrome. Typing h, typing i, and then pressing Tab gave input, input, change, and then focusout, the event for focus leaving. Browsers can differ a little when Enter is pressed, so test the forms you build.
input event right away. Pressing Tab leaves the field, so change sends the final value once, and then focusout follows.- Show a live character count while someone types
- Save a note's title once the person has finished editing it
- Filter a list while the search box changes
- React when a checkbox is ticked
- Show the current volume while someone drags a slider
- Save a shipping speed when an option is picked
Put each job under the event that fits it best.
submit and preventDefault
INTERACTIVEThe submit event belongs to the form, not to the button. Three things can start a submit:
- clicking a submit button,
- pressing Enter in a text field of the form,
- calling
form.requestSubmit()from your code.
The event bubbles: a listener on a parent element hears it too. It is also cancelable: your code can stop what the browser would do next. If your code does not stop it, the browser does three things:
- It checks the form's rules, such as
requiredfields. This check is called constraint validation. - It sends the form's data to the server.
- It loads the server's answer as a new page.
At a front desk, you hand over a filled-in paper form. The desk takes it and sends you on to the next room. In a JavaScript app, you often want to say, "Wait, I'll handle this myself." Then your code reads the form and sends the data. Saying "wait" is preventDefault().
- In real life: The filled-in paper form
- In JavaScript: The form and its fields
- In real life: The desk takes it and sends you to the next room
- In JavaScript: The browser sends the form and loads a new page
- In real life: You say, "Wait, I'll post it myself"
- In JavaScript:
event.preventDefault() - In real life: The tray you dropped it in: Draft or Final
- In JavaScript:
event.submitter, the button that started the submit
form.addEventListener("submit", (event) => { event.preventDefault(); const button = event.submitter; const intent = button?.value ?? "implicit"; status.textContent = "Saved by " + intent + ": " + form.elements.title.value;}); requestButton.addEventListener("click", () => form.requestSubmit());skipButton.addEventListener("click", () => form.submit());- No submits yet.
Click a button, or press Enter in the Title field.
preventDefault(), so the buttons and Enter never leave this page. form.submit() goes to a hidden iframe, a small hidden page, so the demo stays safe.In the lab, pressing Enter in the Title field also submits the form. That works because the form has a submit button. A form with only one text field can also submit on Enter.
event.submitter is the button that started the submit. Here it tells Save draft apart from Publish. It can be null, for example after requestSubmit() with no button given. So read it with ?., the optional chaining operator. It gives undefined instead of an error when the left side is null.
form.addEventListener("submit", (event) => { event.preventDefault(); const submitter = event.submitter; const data = new FormData(event.currentTarget); sendForm(data, submitter?.value ?? "save");});- Line 1 listens for
submiton the form. - Line 2 calls
event.preventDefault(), so the browser does not load a new page. - Line 3 keeps the button that started the submit.
- Line 4 collects the name and value of every field with
new FormData(...).event.currentTargetis the form, the element the listener is on. - Line 5 sends the data. If there was no button, it uses
"save".
event.preventDefault(), the page stays, and your own code, here sendForm(), sends the data.Two methods with similar names do very different things:
form.requestSubmit()acts like a click on a submit button. It checks the form's rules and sends thesubmitevent.form.submit()is older. It sends the form at once. It skips the rule check, and nosubmitevent happens.
const demo = document.createElement("form");
demo.innerHTML = '<input name="name" required><button>Send</button>';
document.body.append(demo);
const target = document.createElement("iframe");
target.name = "submit-target";
target.hidden = true;
document.body.append(target);
demo.target = target.name;
demo.addEventListener("submit", (event) => {
event.preventDefault();
console.log("requestSubmit -> submit event");
});
demo.requestSubmit();
console.log("requestSubmit -> blocked by validation");
demo.elements.name.value = "Asha";
demo.requestSubmit();
demo.submit();
console.log("submit() -> no submit event");The first requestSubmit(), on line 15, is blocked, because the required name field is empty. Line 17 fills in the name. Now the second requestSubmit(), on line 18, passes the check, and the listener prints its message. Line 19 calls submit(), which skips both the check and the listener. Lines 4 to 8 send that submit to a hidden iframe (a small page inside the page), so it can't leave this lesson.
form.addEventListener("reset", () => {
console.log("reset requested");
});
form.reset();
console.log("The reset event runs before the controls return to defaults.");The reset event runs when the form is asked to go back to its starting values. It runs before the fields change back, so your code can still cancel it. Line 5 calls form.reset(), which starts a reset from code. Don't mix this up with clearing one field by setting its value. That sends no event at all.
const field = { value: "Ada" };
field.value = "";
console.log(field.value);
console.log("No input or change event was created by the assignment.");To keep it short, this example uses a plain object in place of a field. Line 2 sets the value to an empty string, and line 3 prints that empty value. But nothing told any listener about it. If your own code owns the data, update that data directly. Don't fake a user event just so other listeners notice.
cut, copy, and paste
INTERACTIVEThe clipboard is the place where your computer keeps text you copied. The cut, copy, and paste events are ClipboardEvents. While the action happens, the event has a clipboardData object:
- On paste, read the text with
event.clipboardData.getData("text/plain")."text/plain"asks for plain text, with no colors or bold. - On copy or cut, set the text with
clipboardData.setData(...).
If you call preventDefault() on paste, the browser does not put the text in for you. Then your listener must put in the text you want.
const actions = ["copy", "cut", "paste"];
console.log(actions.join(" -> "));
console.log("clipboardData is available during each user action");Each action does something different to the field:
copyleaves the field as it is.cutremoves the selected text, after the event.pasteputs text where the cursor or the selection is.
All three give you clipboardData only during the person's action. It is not a way to read the clipboard at any time.
On your phone, you copy a message and paste it into a chat. Imagine you get one quick look at the text before it appears, so you can tidy it up first. Clipboard events give your form that one quick look, and only while the person is copying, cutting, or pasting.
- In real life: The text you paste
- In JavaScript: Read it with
clipboardData.getData - In real life: The text you copy
- In JavaScript: Set it with
clipboardData.setData - In real life: Stopping the usual paste
- In JavaScript: Call
preventDefault() - In real life: Putting in a tidy version
- In JavaScript: Call
setRangeText(clean)
field.addEventListener("paste", (event) => { const text = event.clipboardData.getData("text/plain"); const clean = text.trim().replace(/\s+/g, " "); event.preventDefault(); field.setRangeText(clean, field.selectionStart, field.selectionEnd, "end");}); field.addEventListener("copy", (event) => { const selected = field.value.slice(field.selectionStart, field.selectionEnd); event.clipboardData.setData("text/plain", selected + "\nSource: demo field"); event.preventDefault();});- No clipboard actions yet.
Paste text with extra spaces, or select some text and copy or cut it.
event.clipboardData only while you paste, copy, or cut. navigator.clipboard is a different tool. It needs a secure page and a click or a key press.The paste listener does four things:
- Line 2 reads the pasted text.
- Line 3 cleans it.
trim()removes the spaces at both ends.replace(/\s+/g, " ")turns each run of spaces, tabs, or new lines into one space. - Line 4 stops the normal paste.
- Line 5 puts the clean text in with
setRangeText, in place of whatever is selected.
The copy listener, on lines 8 to 12, adds a line, "Source: demo field", under the copied text.
"hello form" in the field itself.Never read a person's clipboard when the page loads. To copy from a button, such as "Copy link", use navigator.clipboard.writeText("text"). It works only after a click or a key press, and only on a secure page, such as one served over https.
Focus in forms
INTERACTIVEFocus is where your keyboard typing goes. Only one field has focus at a time. Four events report focus moving:
focus: a field got focus.blur: a field lost focus.focusin: the same asfocus, but it bubbles.focusout: the same asblur, but it bubbles.
Because focusin and focusout bubble up to the form, one listener on the form hears every field. It can highlight the group of fields the person is in. It can also check a field when the person leaves it.
blur stays on the field, so a listener on the form never runs. focusout bubbles up, so one listener on the form hears every field.form.addEventListener("focusin", (event) => { event.target.closest("fieldset")?.classList.add("active");}); form.addEventListener("focusout", (event) => { event.target.closest("fieldset")?.classList.remove("active"); validateField(event.target);}); form.addEventListener("input", (event) => { if (event.target.dataset.touched === "true") validateField(event.target);});focusin, the check on focusout, and the new check on each input.Move between the fields. A field is checked only after you leave it once.
focusin and focusout listeners on a plain form.A polite way to check a field has three steps:
- Don't show an error before the person has used the field.
- When they leave the field (
blurorfocusout), mark it as "touched" and check it. - After that, check it again on every
input, so the error goes away as soon as they fix it.
The autofocus attribute puts focus in a field when the page opens. It can help on a tiny page, like a search page. But it can also move a screen reader user away from where they were reading, or open the phone keyboard when nobody asked for it. Use it rarely, and test it.
Each row is one focus event: whether it bubbles up to the form, and a good use for it.
| Event | Does it bubble up to the form? | A good use |
|---|---|---|
focus | No. | React when one field gets focus. |
blur | No. | React when one field loses focus. |
focusin | Yes. | One listener on the form highlights the group the person is in. |
focusout | Yes. | One listener on the form checks the field the person just left. |
Where you will use this
Real forms use these ideas together:
- A search box shows new results on
input. - A settings page saves a menu choice on
change. - A sign-up form stops the normal
submit, sends the data with JavaScript, and shows a "Thanks" message on the same page. - A payment form cleans a pasted name, checks fields on blur, and checks them again on input.
search.addEventListener("input", () => { renderResults(search.value);}); email.addEventListener("focusout", () => { email.dataset.touched = "true"; showEmailError(email.validity.valid ? "" : "Enter an email address.");}); email.addEventListener("input", () => { if (email.dataset.touched === "true") showEmailError("");}); form.addEventListener("submit", (event) => { event.preventDefault(); save(new FormData(form), event.submitter?.value ?? "save");});- Lines 1 to 3: every edit in the search box shows new results.
- Lines 5 to 8: when focus leaves the email field, mark it as touched. Show an error if the email is not valid.
email.validity.validistruewhen the field passes its rules. - Lines 10 to 12: once the field is touched, each new edit clears the error.
- Lines 14 to 17: the form stops the normal submit and saves the data itself.
Keep what the browser already does well, unless you have a good reason to replace it. People rely on labels, the Tab key order, the browser's rule checks, and the normal submit. Your JavaScript should add to these, not remove them.
Common misconceptions
HONEST BUGS- "
inputruns only for the keyboard." It also runs for paste, drag and drop, undo, voice typing, and any other change the person makes. - "
changealways comes later thaninput." For checkboxes, radio buttons, and menus, both run at once from the same choice. - "Setting
.valuesends an event." When your code sets a value, neitherinputnorchangeruns. - "Submit listeners go on buttons." Put them on the form. Find the button with
event.submitter. - "
form.submit()is the same as clicking submit." It skips the rule check and thesubmitevent.requestSubmit()is the one that acts like a click. - "Code can read the clipboard at any time." Browsers allow it only during a person's action, with permission, on a secure page.
Practice exercises
5 EXERCISESYou type h, type i, then press Tab to leave a text field. Name the four events in order.
console.log(["input", "input", "change", "focusout"].join(" "));Typing two letters gives two input events. Leaving the changed field gives one change. Then focusout bubbles up to the form.
Write a listener that shows how many characters are in a textarea. It should update as the person types.
const message = document.querySelector("#message");
const count = document.querySelector("#count");
// Update count.textContent as the user edits.const message = document.querySelector("#message");
const count = document.querySelector("#count");
message.addEventListener("input", () => {
count.textContent = String(message.value.length);
});A counter must update when the person types, pastes, uses voice typing, or deletes. input runs for all of these.
This code runs only when one button is clicked, and not when someone presses Enter. In a listener on the form, which property tells you which submit button was used?
button.addEventListener("click", () => {
send(form.elements.email.value);
});form.addEventListener("submit", (event) => {
event.preventDefault();
const action = event.submitter?.value ?? "save";
console.log(action, form.elements.email.value);
});A listener on the form hears button clicks and Enter. preventDefault() keeps the page here. event.submitter is the button that started the submit, when there is one.
Guess what the paste cleaner puts in the field for hello form , which has extra spaces at the ends and in the middle.
console.log(" hello form ".trim().replace(/\s+/g, " "));trim() removes the outer spaces. Then each run of spaces in the middle becomes one space. So the field gets hello form.
Write the listeners that check a field when the person leaves it, and then check it again on every edit.
function validate(value) {
return value.includes("@") ? "" : "Include an @ sign.";
}field.addEventListener("focusout", () => {
field.dataset.touched = "true";
showError(validate(field.value));
});
field.addEventListener("input", () => {
if (field.dataset.touched === "true") showError(validate(field.value));
});This waits until the person has had a try. Then it updates the error on every edit while they fix it.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which event is best for a live character counter?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst log = []; log.push("input"); log.push("input"); log.push("change"); console.log(log.join(" > "));Choose an answer to see the explanation.
Question 3 of 7Where does the submit event happen?
Choose an answer to see the explanation.
Question 4 of 7Which sentence about
form.submit()is true?Choose an answer to see the explanation.
Question 5 of 7What does the paste cleaner print?
Read the code, then predictfunction clean(text) { return text.trim().replace(/\s+/g, " "); } console.log(clean(" one two "));Choose an answer to see the explanation.
Question 6 of 7What does
preventDefault()do in a paste listener?Choose an answer to see the explanation.
Question 7 of 7Why put one
focusoutlistener on a form instead of ablurlistener on each field?Choose an answer to see the explanation.
Key takeaways
inputruns on every change the person makes.changeruns when the value is final.submitruns on the form. It bubbles, your code can cancel it, andevent.submittertells you which button started it.requestSubmit()acts like a real submit.form.submit()skips the rule check and thesubmitevent.- Clipboard events give you
clipboardDataduring the person's action.preventDefault()on paste stops the normal paste. focusinandfocusoutbubble, so one listener on the form can highlight groups and check fields.
Final definition: Form events are the browser's messages about what a person does in a form: editing, finishing a value, sending, using the clipboard, and moving focus.
Up next: Form validation.