Input Types, Inputmode and Autocomplete
The right type, inputmode and autocomplete change the on-screen keyboard, the autofill offer and the validation the browser runs — a large UX win for one attribute.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
Which attribute actually changes what a user sees when they tap a field, and what does each of them control?
Someone on a phone taps a field to enter a phone number, and wants a number pad — not a QWERTY keyboard they have to switch out of, on a field where every character is a digit.
Use type="text" for everything and validate the format in JavaScript. Types like email and number bring styling quirks and inconsistent behaviour, so it is simpler to standardise on text and control everything ourselves.
Every mobile user gets a full alphabetic keyboard for a card number, an OTP code, a postcode and a phone number, and has to find the number toggle each time. It is a small friction per field and a measurable one per checkout.
- Every mobile user gets a full alphabetic keyboard for a card number, an OTP code, a postcode and a phone number, and has to find the number toggle each time. It is a small friction per field and a measurable one per checkout.
- Autofill has nothing to key off. The browser will not offer the saved address, so the user types twelve fields by hand — the most common reason a mobile checkout is abandoned that is entirely within frontend control.
- The
type="password"behaviours you did not think about disappear withtype="text": masking, exclusion from spell-check and autocorrect, and the browser's own "save this password" prompt. - No native constraint applies, so
requiredis the only thing left and every format rule becomes bespoke JavaScript that has to run before submit and stay in sync with the server's rules (Native Validation and Its Limits). - Assistive technology loses the type information too. A field announced as "edit" instead of "email, edit" gives no clue about what is expected.
- Spell-check underlines appear under usernames, licence keys and IDs, and autocapitalise turns an email address into
Name@example.comon iOS.
What is actually happening
In the browser, not in the framework.
typeis the primary switch. It changes the control's behaviour (masking, stepping, a date picker), its constraint (anemailvalue must parse as an email address), its role on the accessibility tree, and it is the strongest hint to the on-screen keyboard.inputmodechanges only the on-screen keyboard layout, and nothing else. It exists precisely for the case where the value is digits but the type must staytextbecause it is not a number — a credit card, a postcode, a one-time code.autocompleteis a vocabulary of standardised tokens (email,given-name,street-address,postal-code,cc-number,one-time-code,current-password,new-password). It tells the browser what the field *means*, which is what drives the fill offer.enterkeyhintlabels the virtual keyboard's action key —go,next,search,send,done— which is a promise about what pressing it does.- Autofill writes values programmatically. It fires
inputandchange, but nokeydown,keypressorbeforeinputfor typed characters, and it can fill several fields in one go including ones the user has not focused. type="number"is a special case worth knowing: it means "a number" in the mathematical sense, so it accepts exponent notation, strips leading zeros, exposes a spinner, and can silently return an empty string for input the user can see in the field. Identifiers that happen to be digits are not numbers.
What this makes the browser do
And which of it is avoidable.
- Selecting and rendering a keyboard layout, which on mobile is the single most visible piece of work this decision causes.
- Running the type's own constraint check on the value at validation time — a cheap parse, not a regex you wrote.
- Matching
autocompletetokens against stored profile data and rendering the fill dropdown, sometimes with an OS-level authentication step in front of it. - Rendering native pickers for
date,time,colorandfile, which are OS widgets rather than page content and therefore not styleable or scriptable in the ways page content is. - None of this is main-thread work you need to optimise. The cost of getting it wrong is user time, not browser time.
Four attributes, four different jobs
These attributes are routinely confused with each other, and the confusion produces a specific bug: a team adds type="number" hoping for a number pad, and gets a spinner, a scroll-wheel hazard and lost leading zeros as well.
Separating what each one controls makes the right combination obvious for any given field.
- Digits that are not a number →
type="text"+inputmode="numeric"+ anautocompletetoken. - A quantity you would do arithmetic on →
type="number"withmin,maxandstep. - A password being entered →
autocomplete="current-password"; a password being created →new-password. - A search field →
type="search"+enterkeyhint="search", which also gives the platform clear affordance.
| Attribute | Changes behaviour? | Changes the keyboard? | Changes validation? | Changes semantics? |
|---|---|---|---|---|
type | Yes — masking, stepping, pickers, file selection | Yes, strongly | Yes — the type's own constraint | Yes — role and announced type |
inputmode | No | Yes — this is its only job | No | No |
autocomplete | Yes — enables fill and save offers | Indirectly, via what the field means | No | Weakly — conveys purpose |
enterkeyhint | No | Labels the action key only | No | No |
A checkout field, attribute by attribute
pattern="[0-9]*" alongside inputmode is a legacy accommodation for older iOS versions that only honoured the pattern; on current engines inputmode alone is sufficient, and the pattern is harmless but no longer load-bearing.Below is the same set of fields with and without the attributes. The markup difference is small; the mobile difference is the gap between a two-tap fill and thirty seconds of typing.
Note the card number in particular: it is digits, but it is emphatically not a number, so type stays text and inputmode does the keyboard work.
1<label for="name">Full name</label>2<input id="name" name="name" type="text"3 autocomplete="name" autocapitalize="words" />4 5<label for="postcode">Postcode</label>6<input id="postcode" name="postcode" type="text"7 inputmode="numeric" autocomplete="postal-code"8 autocapitalize="characters" spellcheck="false" />9 10<label for="phone">Phone</label>11<input id="phone" name="phone" type="tel"12 autocomplete="tel" enterkeyhint="next" />13 14<label for="card">Card number</label>15<input id="card" name="card" type="text"16 inputmode="numeric" pattern="[0-9\s]{13,19}"17 autocomplete="cc-number" spellcheck="false"18 enterkeyhint="done" />19 20<label for="otp">Verification code</label>21<input id="otp" name="otp" type="text"22 inputmode="numeric" autocomplete="one-time-code"23 maxlength="6" />The one-time-code token is the one people are most surprised by: on supported platforms the OS reads the code from the incoming message and offers it above the keyboard, removing an app switch entirely.
When the browser fills the form for you
Autofill is the input path most application code forgets, and the resulting bugs share a shape: values are present in the DOM and visible to the user, while the application believes the form is empty.
The rule that prevents all of them is to derive from value-change events rather than from keyboard events, and to accept that several fields can change in one turn of the event loop.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Password manager fills email and password | Submit button stays disabled | Enable logic listens on keyup, which autofill never fires | Listen on input and change, or read form.checkValidity() on those events (Native Validation and Its Limits). |
| Address profile fills nine fields at once | Dependent city/region select is not repopulated | The dependency was wired to a single field's handler and assumes one change at a time | Recompute derived state from the whole form on any change, not per field (Derived State). |
| Browser fills a field in a dark-themed form | Text becomes unreadable on a pale background | The browser applies its own filled-field background, which the theme did not anticipate | Style the filled state explicitly and check contrast in both themes (Contrast, Colour and Motion). |
| Change-password screen | Manager fills the old password into "new password" | Both fields carry current-password, or no token at all | Use current-password for the old field and new-password for the new ones so the manager offers to generate and save. |
| OTP split across six one-character inputs | Paste fills only the first box; platform code suggestion does nothing | Each box is a separate control, so the value is never a single six-character entry | One field with autocomplete="one-time-code", styled to look segmented if the design requires it. |
| Controlled React input with a value the framework has not seen | Field visibly resets moments after autofill | A render writes the stale state value back over the browser's fill | Sync from input/change events, and read the DOM value on submit as the source of truth (Controlled vs Uncontrolled Inputs). |
How to build it
Most important first.
- Choose
typefor what the value is:email,tel,url,password,search,date,file,checkbox,radio. Reach fortextwhen none of them describes it, not as a default. - Add
inputmode="numeric"(pluspattern="[0-9]*"for older iOS) to digit strings that are not numbers: card numbers, postcodes, OTP codes, account references. - Add
autocompletetokens to every field a browser could plausibly know. Names, addresses, emails, phone numbers, card fields and both password variants. This is the highest ratio of user benefit to keystrokes in the whole module. - Distinguish
autocomplete="current-password"fromnew-password. The first asks the manager to fill; the second asks it to generate and offer to save. Getting them backwards is why "your site keeps filling my old password on the change-password screen" happens. - Use
autocomplete="one-time-code"on OTP fields so the platform can offer the code from the incoming message instead of making the user switch apps and memorise six digits. - Set
enterkeyhintwhere the action is not obvious, and make sure the key does what the hint claims. - Treat autofill as a first-class input path: listen for
inputandchange, never only for keyboard events, and re-run derived state and enable/disable logic when they fire (State Synchronization). - Turn off the assistants that harm specific fields:
autocapitalize="none"andautocorrect="off"on usernames, emails and codes;spellcheck="false"on identifiers.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- The type contributes to the announced role and to what the user is told to expect. "Email, edit" and "Phone, edit" carry real information that "edit" does not.
- Autofill can populate fields the user never focused. Some screen readers announce the change and some do not, so a form that silently gains nine values should not also change layout or enable a button without any announcement (Live Regions and Announcement).
- Native date, colour and file pickers are OS widgets with their own accessibility behaviour, usually better than a custom equivalent and always more familiar to the user's assistive technology.
inputmodematters for switch and voice users too, not only for the visible keyboard: dictation into a numeric field behaves differently from dictation into a text field on several platforms.- A field whose expected format is non-obvious needs that format in text associated with the field via
aria-describedby, not in a placeholder that vanishes on focus (Errors People Can Actually Perceive).
What can go wrong
type="number"on a phone number, ID or postcode. The spinner appears, scroll-wheel over the focused field silently changes the value, leading zeros vanish, and the value is empty for input the user can see.autocomplete="off"applied form-wide out of caution. Browsers increasingly ignore it for credentials and address data precisely because it was over-used, so it buys nothing and signals nothing — and where it is honoured, it degrades the experience for the users it affects most.- Autofill styling: browsers apply their own background to filled fields, and a dark theme built without accounting for it produces yellow-on-white text that is unreadable until the field is edited.
- Multi-box OTP inputs — six separate one-character fields — which break paste, break
one-time-codefill, and produce six focus stops with confusing announcements. One field withinputmode="numeric"andautocomplete="one-time-code"is better in every dimension except the design mock. - Validation logic bound to
keyup. It never runs for pasted or autofilled values, so a form filled by a password manager shows a disabled submit button with no visible reason. type="date"used where the design demands a custom picker. The native control is not styleable across engines, and the custom replacement inherits the entire date-picker keyboard and announcement contract (Accessible Component Patterns).
- Autofill firing
inputandchangefor several fields in one turn, so a cross-field rule evaluated after the first event runs against a form state that is already out of date. - A platform one-time-code suggestion filling the field while a re-render from a previous state update is still pending, which can write the stale empty value back over it (Controlled vs Uncontrolled Inputs).
- An
autocompletefill arriving after a validation pass has already run, leaving the submit button disabled against values that are now present (Native Validation and Its Limits).
type="password"is a presentation and integration feature, not a security one. It masks characters and opts the field out of spell-check and speech-to-text upload, but the value is plain text in the DOM and in memory.- Autofilled values are user data sitting in your DOM. Analytics, session replay and third-party scripts on the page can read them unless you exclude the fields explicitly (Session Replay and the Privacy It Costs).
- Card fields filled by the browser are still card fields in your page's origin. If you are not intending to handle card data, the fields should live in a payment provider's iframe so the value never enters your document at all.
- No attribute here is a validation guarantee.
type="email",maxlength,patternandinputmodeare all editable in devtools, and the field can be removed entirely before submit (What the Frontend Is Responsible For in Auth).
- "
inputmodeandtypedo the same thing."typechanges behaviour, constraints and semantics;inputmodechanges only the keyboard. They are complementary, and the common correct pairing istype="text"withinputmode="numeric". - "
type="email"validates email addresses." It checks a permissive syntactic shape. It does not check that the domain exists, that the mailbox is real, or that the user did not typo their own address. Only a delivered message proves an address. - "
autocomplete="off"protects sensitive fields." It is widely ignored for credentials, it does not stop extensions, and where it works it mostly harms the user. Sensitive data should not be in your DOM in the first place. - "Autofill is an edge case." It is how a large share of mobile users complete addresses and credentials. A form that mishandles it is broken for them, not degraded.
- "We validate in JavaScript so the type does not matter." The type also picks the keyboard, drives the fill offer, and names the field for assistive technology. Validation is the least of what it does.
Measuring it, and what changes in the field
- A real phone, or the device emulation mode in devtools with a touch keyboard — this is one of the few frontend behaviours a desktop browser cannot show you.
- Field-level analytics: time per field and abandonment per field. A field where mobile users take three times as long as desktop users is usually a keyboard-type problem (Analytics Events That Answer a Question).
- Checking the fill offer directly: save a profile in the browser, then open the form and see whether the dropdown appears and fills the fields you expect.
- The Elements panel for the applied attributes, since frameworks and design-system wrappers routinely drop attributes they do not know about.
- On desktop with a physical keyboard,
typeandinputmodeare nearly invisible — which is exactly why they get skipped by teams who test on desktop. - On mobile, they are among the most visible decisions in the form. Keyboard, fill offer, action key and picker are all downstream of them.
- Across platforms, keyboard layouts for the same
inputmodediffer: iOS and Android rendernumericanddecimaldifferently, and some keyboards ignore hints entirely. - For a user with a password manager or an address profile, correct
autocompletetokens can reduce a twelve-field form to two taps. For a user with neither, the tokens change nothing — so field-level metrics will show a bimodal distribution, not a uniform improvement. - In locales your form was not designed for, assumptions embedded in a
pattern— postcode shape, phone format, name order — become validation failures for legitimate values (Internationalization).
- Native pickers cannot be styled to match a design system, and their behaviour differs across engines. Consistency across browsers and fidelity to the platform are genuinely in tension here.
type="number"is useful for real numeric input with a step, and annoying everywhere else. There is no single correct answer for "digits"; the choice depends on whether arithmetic makes sense on the value.- Adding
autocompletetokens means agreeing with the browser about what a field means, which occasionally forces a field to be split — onestreet-addressfield intoaddress-line1andaddress-line2— for the fill to work well. - Supporting autofill as an input path means your state layer cannot assume a keystroke preceded every value change, which rules out some tempting optimisations.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe attributes and the
autocompletetoken vocabulary are specified in HTML and understood by every current engine; what varies is how strongly each browser acts on them. - PLATFORM-SPECIFICKeyboard rendering is the OS keyboard's decision, not the browser's: iOS and Android show different layouts for the same
inputmode, third-party keyboards may ignore the hint entirely, and desktop browsers ignore it by definition since there is no on-screen keyboard to change. - SPEC-EVOLVINGThe
autocompletetoken list and browsers' willingness to honourautocomplete="off"have both changed repeatedly; treat the current token set as a living list to check rather than a fixed one to memorise.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — autofill is close to untestable in a headless browser, which makes it a good example of a behaviour that needs a device lab or a manual check in the release process rather than a unit test.