The form element and submission
What the browser does the moment a user clicks submit — with no JavaScript at all.
You can build an entire form-handling experience in JavaScript: stop the submit, serialize the values, send a fetch, parse the response, render the result. People do that every day. But underneath, the browser already knows how to do all of it — and it has known since 1994.
This lesson is about that built-in machinery: what happens when a <form> is submitted by a real <button type="submit"> with no JavaScript handler at all. Once you can see the default behaviour clearly, every JavaScript form library you have ever used starts to look like an enhancement on top of it, not a replacement for it.
Anatomy of a form
A form is a <form> element wrapping any number of inputs and a button. Two attributes on the form decide what happens at submit: action (where the data goes) and method (how it gets there).
<form action="/contact" method="post"> <label for="name">Name</label> <input id="name" name="name" type="text" required> <label for="msg">Message</label> <textarea id="msg" name="msg" required></textarea> <button type="submit">Send</button> </form>
Click "Send" and the browser does this, in order:
- Walks the form, collecting every named control that has a value.
- Builds a
name=value&name=valuepayload from those pairs. - Sends an HTTP request to
/contact(theaction) using POST (themethod), with the payload in the request body. - Replaces the current page with whatever the server sends back.
That entire choreography is the browser's. You wrote no JavaScript. The form just works.
A useful picture: a form is a paper questionnaire on a clipboard. The fields are the lines you fill in; the submit button is the box at the bottom that says "drop in mailbox X". The action is the mailbox address; the method is whether the form is mailed in a sealed envelope (POST) or shouted across the room (GET).
GET vs POST
Two methods, two very different behaviours.
method="get" puts the form data into the URL as a query string. Submit ?name=Maya&msg=hello and the browser navigates to /contact?name=Maya&msg=hello. The data is visible in the address bar, in browser history, in server logs. Bookmarkable. Cacheable.
method="post" puts the form data in the body of the request. The URL stays clean. The data is not in browser history. Use POST for anything that changes state on the server.
The rule of thumb: POST when the form does something (signs up, posts a comment, places an order). GET when the form is a query (search, filter, sort).
<!-- A search form belongs in GET — bookmarkable results URL. --> <form action="/search" method="get"> <label for="q">Search</label> <input id="q" name="q" type="search"> <button type="submit">Go</button> </form>
Submit "sourdough" and you land on /search?q=sourdough. Bookmark that URL and you can return to the same results. Share it. Refresh it. All for free.
<!-- A signup form belongs in POST — never in the URL. --> <form action="/signup" method="post"> <label for="email">Email</label> <input id="email" name="email" type="email" required> <button type="submit">Sign up</button> </form>
If you used GET here, the email would land in the URL — bookmarkable, logged, cached. POST keeps it in the body where it belongs.
GET requests can be repeated or prefetched by the browser. If submitting your form has a real effect (creating a user, charging a card), never use GET — a browser preview, a search-engine crawler, or a "back button" can fire it more than once.
<form action="/cart/remove" method="get">. A search engine crawls your site. What is the most likely problem?enctype, the body format
The form's enctype attribute names the format of the request body. Three legal values, each with a job.
application/x-www-form-urlencoded— the default. Fields are encoded asname=value&name=value, just like a query string. Used for plain text data.multipart/form-data— the format you need when uploading files. Each field is a separate "part" of the request body, with its own headers. Required for<input type="file">.text/plain— almost never useful; sends the data as plain text. Mostly for debugging.
<form action="/upload" method="post" enctype="multipart/form-data"> <label for="avatar">Profile picture</label> <input id="avatar" name="avatar" type="file"> <button type="submit">Upload</button> </form>
The enctype="multipart/form-data" is mandatory when the form has a file input. Forget it and the server receives the filename but not the file's bytes.
For the other 99% of forms — text fields, selects, checkboxes — the default enctype is what you want. Do not set it explicitly; it adds noise.
target and form behaviour
The target attribute decides where the response loads. It works the same way as on <a> tags.
target="_self"— the default. The current tab navigates to the response.target="_blank"— the response opens in a new tab; the current tab keeps the form.target="iframe-name"— a named iframe receives the response. (Used to be common; rare now outside legacy.)
<!-- Open the thank-you page in a new tab so the user keeps reading. --> <form action="/subscribe" method="post" target="_blank"> <input name="email" type="email" required> <button type="submit">Subscribe</button> </form>
Most forms leave target alone — replacing the page on submit is what users expect. The exception is the "do not interrupt the user's flow" case (a newsletter sign-up at the bottom of an article), where opening the confirmation in a new tab can be polite.
Why this still matters
Most production forms today are submitted via JavaScript: event.preventDefault(), fetch(), then update the DOM in place. So why bother understanding the no-JS version at all?
Three reasons:
-
Progressive enhancement. A JavaScript-enhanced form built on top of a real
<form>still works if the JS fails to load (slow connection, blocked script). One built on a<div>andonclickis invisible without JS. -
Server actions and form actions. Modern frameworks (React Server Components, Remix, vanilla browser API) let you pass a server function as the form's
action. The framework wires up the submission for you, but only if the underlying<form>shape is correct. -
The browser's machinery is what assistive tech keys off. Submit on Enter, focus the first invalid field, fire the right events — all of it is part of the form contract. Skip the
<form>and you have to rebuild it by hand.
Build the form so it works without JavaScript first. Then layer JavaScript on top — preventDefault() on submit, fetch the request yourself, update the page in place. The base case is your safety net; the JS is the enhancement.
<input type="file"> to an existing form but forget to change the form's enctype. What does the server receive?<form> element — we'll just listen for click on the button and call fetch". What is the most concrete thing you lose?