FormsGENERALPLATFORM-SPECIFICSPEC-EVOLVING

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.

The question

Which attribute actually changes what a user sees when they tap a field, and what does each of them control?

The user intent

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.

The obvious build

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.

Why it breaks

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.

How it breaks in a real browser
  • 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 with type="text": masking, exclusion from spell-check and autocorrect, and the browser's own "save this password" prompt.
  • No native constraint applies, so required is 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.com on iOS.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • type is the primary switch. It changes the control's behaviour (masking, stepping, a date picker), its constraint (an email value must parse as an email address), its role on the accessibility tree, and it is the strongest hint to the on-screen keyboard.
  • inputmode changes only the on-screen keyboard layout, and nothing else. It exists precisely for the case where the value is digits but the type must stay text because it is not a number — a credit card, a postcode, a one-time code.
  • autocomplete is 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.
  • enterkeyhint labels 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 input and change, but no keydown, keypress or beforeinput for 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 autocomplete tokens 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, color and file, 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" + an autocomplete token.
  • A quantity you would do arithmetic on → type="number" with min, max and step.
  • 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.
AttributeChanges behaviour?Changes the keyboard?Changes validation?Changes semantics?
typeYes — masking, stepping, pickers, file selectionYes, stronglyYes — the type's own constraintYes — role and announced type
inputmodeNoYes — this is its only jobNoNo
autocompleteYes — enables fill and save offersIndirectly, via what the field meansNoWeakly — conveys purpose
enterkeyhintNoLabels the action key onlyNoNo

A checkout field, attribute by attribute

PLATFORM-SPECIFICThe 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.

Address and payment fields with intent declared
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.

Autofill failure modes and their real cause
TriggerSymptomCauseResponse
Password manager fills email and passwordSubmit button stays disabledEnable logic listens on keyup, which autofill never firesListen on input and change, or read form.checkValidity() on those events (Native Validation and Its Limits).
Address profile fills nine fields at onceDependent city/region select is not repopulatedThe dependency was wired to a single field's handler and assumes one change at a timeRecompute derived state from the whole form on any change, not per field (Derived State).
Browser fills a field in a dark-themed formText becomes unreadable on a pale backgroundThe browser applies its own filled-field background, which the theme did not anticipateStyle the filled state explicitly and check contrast in both themes (Contrast, Colour and Motion).
Change-password screenManager fills the old password into "new password"Both fields carry current-password, or no token at allUse 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 inputsPaste fills only the first box; platform code suggestion does nothingEach box is a separate control, so the value is never a single six-character entryOne 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 seenField visibly resets moments after autofillA render writes the stale state value back over the browser's fillSync 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 type for what the value is: email, tel, url, password, search, date, file, checkbox, radio. Reach for text when none of them describes it, not as a default.
  • Add inputmode="numeric" (plus pattern="[0-9]*" for older iOS) to digit strings that are not numbers: card numbers, postcodes, OTP codes, account references.
  • Add autocomplete tokens 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" from new-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 enterkeyhint where 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 input and change, 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" and autocorrect="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.
  • inputmode matters 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

Failure modes
  • 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-code fill, and produce six focus stops with confusing announcements. One field with inputmode="numeric" and autocomplete="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).
What can arrive out of order
  • Autofill firing input and change for 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 autocomplete fill arriving after a validation pass has already run, leaving the submit button disabled against values that are now present (Native Validation and Its Limits).
Security
  • 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, pattern and inputmode are all editable in devtools, and the field can be removed entirely before submit (What the Frontend Is Responsible For in Auth).
Misreads
  • "inputmode and type do the same thing." type changes behaviour, constraints and semantics; inputmode changes only the keyboard. They are complementary, and the common correct pairing is type="text" with inputmode="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

How you would see this
  • 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.
Slow device, slow network, large data, old tab
  • On desktop with a physical keyboard, type and inputmode are 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 inputmode differ: iOS and Android render numeric and decimal differently, and some keyboards ignore hints entirely.
  • For a user with a password manager or an address profile, correct autocomplete tokens 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).
What this costs
  • 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 autocomplete tokens means agreeing with the browser about what a field means, which occasionally forces a field to be split — one street-address field into address-line1 and address-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 autocomplete token 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 autocomplete token list and browsers' willingness to honour autocomplete="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.

Domains that do not exist yet
  • 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.