Back to blog
tutorial

HTML Form Input Types: What Each One Sends to Your Server

All 22 HTML form input types, what each one actually sends to your server, and why fields go missing: unchecked boxes, disabled inputs, and enctype.

J
Jesper Christiansen
19 min read

An HTML form has two jobs. It has to make sense to the person filling it in, and it has to hand your server something usable. The type attribute is where those two jobs meet, and it’s where most form bugs start.

Most references stop at the first job. They’ll tell you that type="date" renders a date picker and type="email" shows a keyboard with an @ key. That’s true, and it’s the easy half. The half that costs you an afternoon is the second one: what actually arrives at the other end.

We run a form endpoint, so we see the other end all day. This guide covers all 22 input types and, for each, the thing the browser reference leaves out: the value that lands in the request body, and the cases where nothing lands at all.

Table of Contents

The complete list of HTML form input types

There are 22 valid values for the type attribute on an <input> element. Here they are with what each one is for:

Type What it’s for
text Single-line text. The default when type is missing or unrecognized.
email An email address, with browser-side format checking.
password Single-line text with the characters obscured on screen.
search A search term. Renders with a clear button in most browsers.
tel A phone number. No format validation, since numbers vary too much by country.
url An absolute URL, with browser-side format checking.
number A numeric value with a spinner control.
range A number where the exact value doesn’t matter. Renders as a slider.
checkbox An independent on/off choice.
radio One choice from a named group.
file One or more files from the user’s device.
date A calendar date with no time.
time A time with no date and no timezone.
datetime-local A date and time with no timezone.
month A year and month.
week A year and an ISO week number.
color A color, via the platform color picker.
hidden A value the user never sees and can’t edit in the UI.
submit A button that submits the form.
reset A button that restores every field to its default value.
button A button with no behavior of its own. Needs JavaScript.
image A submit button rendered as an image.

If you set type to something the browser doesn’t recognize, it falls back to text. That’s why a typo like type="emial" produces a working field that quietly skips email validation.

Type controls the UI. Name controls the data.

This is the distinction worth internalizing before anything else.

The type attribute decides how the browser renders and validates a field. The name attribute decides the key that field is stored under. They’re independent, and only one of them is required for the data to arrive.

<input type="email" placeholder="you@example.com">

That field validates as an email and submits nothing at all. No name, no key, no data. The browser skips it when it builds the request, and it does so without an error. The form appears to work.

<input type="email" name="email" placeholder="you@example.com">

That one arrives as email=you@example.com.

A missing name is the single most common reason a field doesn’t reach the server, and it’s invisible in the browser. The field renders, accepts input, passes validation, and contributes nothing. If you take one thing from this page, take that.

The six rules that decide what gets submitted

When you submit a form, the browser builds what the HTML standard calls the form data set: the list of name/value pairs that go into the request. It walks every field in the form and decides whether to include it.

Per the W3C’s “Constructing the form data set” algorithm, a field is skipped entirely when any of these is true:

  1. It has a <datalist> element as an ancestor.
  2. It is disabled.
  3. It is a button, but not the button that submitted the form.
  4. It is a checkbox whose checkedness is false.
  5. It is a radio whose checkedness is false.
  6. It has no name attribute, or an empty one (image buttons excepted).

Everything else contributes a pair. (The standard lists one more condition, for <object> elements not using a plugin, which you can safely ignore in 2026.)

Why fields go missing

Those six rules explain nearly every “my form isn’t sending X” report we see.

Disabled is not readonly. Both stop the user editing a value, but they behave differently on submit. Per MDN’s disabled reference, disabled controls “are not submitted with the form,” whereas “read-only controls can still function and are still focusable.”

<input type="text" name="plan" value="pro" disabled>   <!-- never arrives -->
<input type="text" name="plan" value="pro" readonly>   <!-- arrives as plan=pro -->

If you’re greying out a field but still need its value, readonly is the one you want. If you genuinely don’t want the value, disabled is correct. Just don’t be surprised when the key is absent.

Only the clicked button submits. A form with three submit buttons sends the name and value of exactly one of them: the one the user pressed. That’s how you tell which action was taken:

<button type="submit" name="action" value="save">Save</button>
<button type="submit" name="action" value="publish">Publish</button>

One of action=save or action=publish arrives. Never both, never neither. The exception is a form submitted by JavaScript, where no button is the submitter and no action key is sent at all. That last case catches people out when they add a fetch() handler to a form that previously worked.

Text and text-like types

text, email, password, search, tel, and url all submit the same way: the raw string the user typed, under the field’s name.

<input type="email" name="email" value="ada@example.com">
<!-- arrives as: email=ada@example.com -->

What differs is the keyboard on mobile and whether the browser checks the format before allowing submission. email and url validate. tel deliberately doesn’t, because phone number formats vary too much between countries to check usefully.

Two things worth knowing:

type="password" is a display feature, not a security one. The value travels as an ordinary name/value pair in the request. The dots on screen protect against someone reading over a shoulder, nothing more. What protects the value in transit is HTTPS. And per MDN’s guidance on sending form data, you should “never use the GET method” for a password. It would put the value straight in the URL bar, browser history, and server logs.

Browser validation is not server validation. type="email" asks the browser to check the format. Anyone can edit the HTML in devtools, or skip the browser entirely and POST to your endpoint with curl. Treat every public form as an internet-facing API that happens to have an HTML front end. See our guide to validating input in the browser for where the line sits.

<textarea> belongs in this group too. It isn’t an <input> at all. It’s its own element, with the value between the tags rather than in a value attribute, but it submits exactly like type="text", newlines included.

<textarea name="message">Line one
Line two</textarea>
<!-- arrives as: message=Line one\r\nLine two -->

Choice types: checkbox, radio, and select

This is where the wire format stops matching the UI.

A checked checkbox submits name=value. If you omit value, the value is the string "on".

<input type="checkbox" name="newsletter">
<!-- checked:   newsletter=on -->
<!-- unchecked: nothing at all -->

That "on" default surprises people who expected true. Set an explicit value if you want something more useful:

<input type="checkbox" name="newsletter" value="yes">

Multiple checkboxes sharing a name produce multiple entries with the same key.

<input type="checkbox" name="topics" value="rails">
<input type="checkbox" name="topics" value="js">
<input type="checkbox" name="topics" value="css">

Tick the first two and the request carries topics=rails and topics=js: two separate pairs with the same name. What your code receives then depends entirely on the parser, and this is where data quietly disappears.

Rack, which sits under Rails, keeps only the last value. We checked while writing this:

Rack::Utils.parse_nested_query("topics=rails&topics=js")
# => {"topics" => "js"}          the first value is gone

Rack::Utils.parse_nested_query("topics[]=rails&topics[]=js")
# => {"topics" => ["rails", "js"]}

PHP behaves the same way. So: a group of checkboxes sharing a plain name can silently lose every value but one. If you want all of them, name the field topics[]. This matters more than it looks, and we come back to it below.

Radio buttons work the same way, minus the multiples. Only the checked one in a group submits. If none is checked, the group contributes nothing, which means a required-looking radio group with no default sends no key at all.

<select> submits the value of the chosen <option>, falling back to the option’s text content if it has no value attribute. With the multiple attribute, the standard appends one entry per selected option, exactly like grouped checkboxes.

<select name="plan">
  <option value="free">Free</option>
  <option value="pro">Pro</option>
</select>
<!-- arrives as: plan=pro -->

The unchecked checkbox problem

An unchecked checkbox doesn’t submit an empty value. It submits nothing. The key is absent from the request entirely.

That’s a real distinction, and it bites in two places.

First, on the server, params["newsletter"] is nil, not "" and not false. If your code checks for a falsy value you’re fine; if it expects the key to exist, you get a surprise.

Second, you can’t tell “the user unticked this” apart from “this field wasn’t on the form.” For a settings form where that difference matters, the standard workaround is a hidden field with the same name placed before the checkbox:

<input type="hidden" name="newsletter" value="0">
<input type="checkbox" name="newsletter" value="1">

Unchecked, only the hidden field submits and you get newsletter=0. Checked, both submit and the later value wins in most server-side parsers, giving you newsletter=1. It’s a workaround built on parser ordering rather than a guarantee in the standard, so confirm the behavior in your stack before relying on it.

Date and time types

The date and time types are the clearest case of the display differing from the data.

They always submit a fixed machine format, no matter what the picker shows the user. A browser on a US locale may display 08/02/2026 while submitting 2026-08-02.

Type Submitted format Example
date YYYY-MM-DD 2026-08-02
month YYYY-MM 2026-08
week YYYY-Www 2026-W31
time HH:mm or HH:mm:ss.sss 14:30
datetime-local ISO 8601, no timezone 2026-08-02T14:30

The word to notice is local. datetime-local carries no timezone and no offset. 2026-08-02T14:30 from a user in Copenhagen and the same string from a user in Los Angeles are eleven hours apart in reality and identical in your database. If timezone matters, capture it separately with a hidden field.

Numeric types: number and range

type="number" gives you a spinner and asks the browser to accept only numeric input. Per MDN, a number input “is considered valid when empty and when a single number is entered, but is otherwise invalid”, and browsers differ on whether invalid characters can be typed in the first place (Firefox bug 1398528).

The practical consequence: you can receive an empty string where you expected a number. The value arrives as text like every other form value, so "42" is a string until you cast it, and an empty field gives you "" rather than 0 or nil. Parse and validate it server-side.

type="range" is the opposite problem. It always submits a value, because the slider always has a position:

<input type="range" name="volume" min="0" max="100">
<!-- untouched, arrives as: volume=50 -->

Per MDN, the default is the midpoint of min and max. There’s no “unset” state, so you can’t distinguish a deliberate 50 from a slider the user never touched. If that difference matters, start the slider outside the meaningful range or pair it with a checkbox.

File uploads and why enctype matters

type="file" is the one input type that needs the form configured, not just the field.

A form with a file input needs both method="post" and enctype="multipart/form-data".

<form action="https://www.formbackend.com/f/your-form-endpoint"
      method="post"
      enctype="multipart/form-data">
  <input type="file" name="resume">
  <button type="submit">Send</button>
</form>

The default encoding is application/x-www-form-urlencoded, which is a text format. As MDN puts it, “files are binary data — or considered as such — whereas all other data is text data.” The default encoding has nowhere to put the bytes.

What makes this worth calling out is the failure mode. Leave the enctype off and you don’t get an error. The browser sends the filename as text and drops the contents, so your endpoint receives resume=cv.pdf and no file. The form submits, the success page loads, and the upload is gone. If you’re wiring this up, we’ve covered the full flow in handling file uploads via your form.

Add multiple and the field contributes one entry per selected file, all under the same name.

Hidden, image, and the button types

type="hidden" submits its value with no user interaction and no visual presence. Useful for form IDs, timestamps, and timezone offsets. It is not secret. It’s in the page source, and anyone can change it before submitting.

type="image" is a submit button rendered as an image, and it’s the odd one out in the whole list. It doesn’t submit its value. It submits the click coordinates, as two entries suffixed .x and .y:

<input type="image" name="map" src="map.png" alt="Pick a location">
<!-- arrives as: map.x=50 and map.y=30 -->

It’s also the one type exempt from the name requirement: an image button with no name still submits coordinates, as plain x and y.

type="submit" submits its name and value, but only when it’s the button that was clicked. type="reset" and type="button" never contribute data at all; they’re covered by rule 3 above.

One recent addition worth knowing: the switch attribute turns a checkbox into a native toggle. Per WebKit and MDN it’s Safari-only (17.4+), and browsers without support silently render a normal checkbox. Either way it submits exactly like a checkbox, so it’s safe to use today. The alpha and colorspace attributes on type="color" are in the same position. Safari 18.4 shipped first and they are not yet Baseline. A plain type="color" submits seven-character lowercase hex, defaulting to #000000.

The wire format of every input type

The reference version, for when you just need to know what lands:

Type What arrives at your server
text, search, tel The raw string
email The raw string, format-checked by the browser only
url The raw string, format-checked by the browser only
password The raw string, in plaintext, in the request body
number A string, or "" if the field was left empty
range Always a value; defaults to the midpoint of min/max
checkbox name=value when checked ("on" if no value). Nothing when unchecked
radio name=value for the checked one only. Nothing if none checked
select The chosen option’s value; one entry per selection with multiple
textarea The raw string, newlines preserved
file The file, but only with enctype="multipart/form-data"
date YYYY-MM-DD
month YYYY-MM
week YYYY-Www
time HH:mm
datetime-local ISO 8601 with no timezone
color Seven-character lowercase hex, e.g. #ff0000
hidden Its value, always
image name.x and name.y click coordinates
submit name=value, only if it was the clicked button
reset, button Nothing, ever

What we see at the endpoint

Running a form endpoint means the failures show up as support tickets rather than theory. Three patterns come up often enough to be worth naming.

Bracketed names produce arrays, and arrays break code that expects strings. We learned this one the hard way in July 2026. FormBackend picks up a field named email to decide who an auto-response goes to. When a form posts multiple values under a bracketed name like email[], that value arrives as an array, not a string. Our spam check called .ends_with? on it and raised a NoMethodError, because a non-empty array isn’t blank and slipped straight past the guard clause. The same latent bug was sitting in the notification mailer. We fixed it by coercing array values to the first non-blank string at the source, and the regression tests for it are in our test suite today.

The general lesson is worth more than our specific bug: a field you designed to hold one value can arrive holding several. Bracketed names, checkbox groups, and <select multiple> all do it. If your handler assumes a string, a single pair of square brackets in someone else’s markup is enough to break it.

All-blank submissions are dropped, not stored. If every value in a submission is blank, we don’t persist it. There’s nothing to store and the alternative is an inbox full of empty rows. Combine that with the unchecked-checkbox rule and you get a case that looks like a bug: a form made up only of checkboxes, submitted with none of them ticked, sends no keys at all. There’s nothing to save, so nothing appears in the dashboard. The form is working exactly as specified; the specification is just surprising.

Some names are instructions, not data. Field names prefixed with an underscore configure how we handle the submission rather than becoming answers. _redirect_to sets where the user goes next, _subject sets the notification subject line, and a field named email is picked up as the submitter’s address for auto-responses. Those are stripped from the stored values, so naming an ordinary question _subject means the answer configures an email instead of showing up in your results. The full list is in our special fields documentation.

The honeypot for spam protection works on the same principle in reverse: a field that should always be empty, whose presence tells us something rather than storing something.

A checklist for a field that won&#39;t submit

When a value isn’t arriving, work down this list. It’s ordered by how often each one turns out to be the cause:

  1. Does the field have a name? No name, no data. Silent every time.
  2. Is it disabled? Disabled fields are skipped. You probably want readonly.
  3. Is it an unchecked checkbox or radio? Then there’s no key to find. Check for absence, not for a falsy value.
  4. Is it a file input? The form needs method="post" and enctype="multipart/form-data", or the bytes are dropped and you get a filename.
  5. Is it a button? Only the clicked submitter is sent. Submitting via JavaScript sends none of them.
  6. Is it inside a <datalist>? Those are suggestions, not fields.
  7. Is it inside the <form> element? A field outside the form needs an explicit form="form-id" attribute to be associated with it.
  8. Is the action pointing where you think? If the request isn’t going where you expect, none of the above matters. See the form action attribute and GET vs POST.

If you want to see exactly what a form sends without building a backend to inspect it, that’s the short version of what we do: point your form’s action at a FormBackend endpoint and every submission shows up with its raw keys and values, arrays and all. Useful for debugging, and it keeps working once you stop debugging.

The rules above aren’t complicated, but they aren’t guessable either. The browser decides what to send using a fixed algorithm, and once you know the six conditions that skip a field, most form bugs stop being mysterious and start being a name attribute you forgot.

Frequently asked questions

What are the HTML form input types?

There are 22 values for the type attribute on an <input> element: button, checkbox, color, date, datetime-local, email, file, hidden, image, month, number, password, radio, range, reset, search, submit, tel, text, time, url, and week. The type controls how the browser renders and validates the field. It does not control the key the value is stored under. That comes from the name attribute.

What does an unchecked checkbox submit?

Nothing. An unchecked checkbox is skipped entirely when the browser builds the form data set, so the key does not appear in the request at all. This is different from submitting an empty value. If you need to know that a box was deliberately left unticked, pair it with a hidden input of the same name before the checkbox, or treat a missing key as false on the server.

Why is one of my form fields not being submitted?

The HTML standard skips a field entirely under six conditions that matter in practice: it has no name attribute (or an empty one), it is disabled, it is a checkbox that is not checked, it is a radio that is not checked, it is a button that was not the one clicked to submit, or it sits inside a datalist element. A missing name attribute is by far the most common cause. The field looks fine on the page and silently never reaches the server.

What is the difference between disabled and readonly on a form field?

A readonly field is submitted with the form; a disabled field is not. Both prevent the user from editing the value, but disabled also removes the field from the form data set and makes it unfocusable. If you want the user to see a value they cannot change but you still need that value on the server, use readonly.

Do I need enctype for file uploads?

Yes. A form containing <input type="file"> needs both method="post" and enctype="multipart/form-data". The default encoding is application/x-www-form-urlencoded, which cannot carry binary data. With the default, the browser sends the filename as text and drops the file contents, so the upload fails silently with no error.

What format does input type=date submit?

Always YYYY-MM-DD, regardless of how the date picker displays it to the user. A browser set to a US locale may show 08/02/2026 in the widget while submitting 2026-08-02. The related types follow the same pattern: month submits YYYY-MM, week submits YYYY-Www, time submits HH:mm, and datetime-local submits an ISO 8601 string with no timezone.

Does the input type validate data on the server?

No. type="email" and type="number" only ask the browser to check the value before submitting. Anyone can bypass that by editing the HTML, disabling JavaScript, or posting to your endpoint directly with curl. Input types are a user-experience feature, not a security boundary. Every value still has to be validated on the server.

Add a form backend to your site in minutes

Connect any HTML form to FormBackend and start collecting submissions — no backend code required.

Start free