Dropping an <input type="number"> into your form seems like the semantic, responsible choice for handling numeric data, right until a user accidentally scrolls their mouse wheel and alters a payment amount without noticing. Browsers also silently discard non-numeric characters in these fields, leaving users confused and your validation logic completely broken.
The Verdict: Never use type="number" for non-quantitative data like credit cards, ID numbers, or postal codes.
The Best Alternative: <input type="text" inputmode="numeric" pattern="[0-9]*"> provides the ideal mobile keyboard without the desktop bugs.
Critical UX Bugs: Highly susceptible to accidental scroll-wheel value changes and browser rounding errors on large numbers.
Accessibility Failures: Frequently fails with screen readers like NVDA and dictation software like Dragon NaturallySpeaking.
Why input type="number" is Broken
The Scroll Wheel Bug (And How to Prevent It)
The most dangerous flaw of the number input is its default interaction with the mouse scroll wheel. If a user leaves their cursor hovering over the input field and scrolls down the page, the browser increments or decrements the value instead of scrolling the window. This leads to disastrous data entry errors on financial or medical forms.
You must actively fight the browser to prevent this behavior. The standard approach intercepts the wheel event and forces the input to lose focus the moment a scroll is detected:
const input = document.querySelector('input[type="number"]');
input.addEventListener('wheel', function (e) {
e.preventDefault();
this.blur();
});
Screen Reader Failures (NVDA and Dragon NaturallySpeaking)
The GOV.UK Design System team documented their findings after extensive testing across assistive technologies. Voice dictation software, specifically Dragon NaturallySpeaking, consistently fails to dictate values into number inputs.
Screen readers do not fare much better. NVDA has a documented history of reading type="number" inputs as completely unlabeled elements in its elements list. This leaves visually impaired users guessing what data they are supposed to provide.
Implicit ARIA role="spinbutton" Confusion
When you use the number type, the browser automatically applies an implicit ARIA role="spinbutton". Assistive technology users interpret a spinbutton as an interface where they should use the up and down arrow keys to adjust a value incrementally.
This makes perfect sense for selecting how many tickets you want to buy. It creates massive cognitive friction when a user is simply trying to type out their ZIP code or an account number.
When NOT to Use type="number"
Credit Cards, Postal Codes, and Phone Numbers
A simple rule of thumb: if you do not perform mathematical operations on the data, it is not a number. Credit card sequences, postal codes, and phone numbers are identification strings that happen to consist of digits.
Using the number input for these fields triggers the spinbutton UI and allows the entry of scientific notation characters. You do not increment a phone number, so do not give the browser the UI tools to do so.
<!-- Wrong: credit cards, postal codes, phone numbers -->
<input type="number" name="card" />
<!-- Right: text + numeric keyboard on mobile -->
<input type="text" inputmode="numeric" pattern="\d*" name="card" />
<input type="tel" name="phone" />
Large Numbers and Browser Rounding Issues
Browsers handle type="number" values as floating-point numbers under the hood. Once an inputted sequence exceeds 16 digits, JavaScript's numeric precision limits kick in, and the browser starts rounding the values.
If a user types a 20-digit passport or international bank account number, the browser will quietly alter the last few digits. The user submits the form, and the data is corrupted before it even hits your server.
The GOV.UK Solution: type="text" + inputmode="numeric"
Faced with these accessibility and UX hurdles, the GOV.UK Design System team stopped using type="number" entirely across their public-facing services.
Their standardized approach uses standard text inputs combined with mobile-specific attributes:
<input type="text" inputmode="numeric" pattern="[0-9]*" />
This triggers the large numeric keypad on iOS and Android devices. Desktop users get a standard text field without the dangerous scroll wheel behavior or the confusing spinbutton UI.
Quick Reference: Which Input Type to Use
| Data Type | Recommended Input | inputmode |
Notes |
|---|---|---|---|
| Quantity / age | type="number" |
- | Safe for incrementable values |
| Currency / decimals | type="number" |
- | Add step="0.01" |
| Credit card number | type="text" |
numeric |
Prevents rounding and spinbutton |
| Postal / ZIP code | type="text" |
numeric |
May be alphanumeric by country |
| Phone number | type="tel" |
- | Gets native dial pad on mobile |
| Passport / ID | type="text" |
numeric |
16+ digit precision issues |
How to Use type="number" Correctly (If You Must)
If you are building a simple quantity selector for a shopping cart, type="number" can still work, provided you understand its quirks.
step="any" vs step="0.01" for Decimals
By default, number inputs only accept whole integers. If a user enters 3.14, the browser's constraint validation will flag it as invalid.
To allow decimal entries, you must define the step attribute:
<!-- Currency: strict two-decimal format -->
<input type="number" step="0.01" min="0" />
<!-- Scientific measurements: any decimal value -->
<input type="number" step="any" />
step="0.01" is the right choice for currency, because it enforces a clean two-decimal format. step="any" accepts completely arbitrary decimal values when you have no constraint on precision.
Preventing Scientific Notation (e/E)
The characters e and E are mathematically valid in HTML number inputs to represent scientific notation (e.g., 2e3 for 2000). Browsers allow users to type these letters freely.
If your application only expects standard integers, attach a keydown listener and call preventDefault() for the offending keys:
input.addEventListener('keydown', (e) => {
if (['e', 'E', '+', '-'].includes(e.key)) {
e.preventDefault();
}
});
Note that blocking - also prevents negative numbers, so only add it if your field should be positive-only.
Why pattern and size Attributes Are Ignored
Developers often try to restrict the character count of a number field using size or enforce a specific format using pattern. Both attributes are completely ignored by the browser on type="number".
The number input relies on its own constraint validation model dictated entirely by min, max, and step. If you want to change the visual width of the field, use CSS:
input[type="number"] {
width: 6ch;
}
Server-Side Validation: The Final Safety Net
No matter how many client-side JavaScript listeners or HTML attributes you configure, the frontend remains insecure. The browser's native validation can be easily bypassed by anyone familiar with developer tools.
Malformed values like NaN and Infinity can technically be submitted through poorly configured forms. Your backend must validate, sanitize, and cast all incoming numerical data before it touches your database. Client-side constraints are a UX feature, not a security mechanism.
If you are dealing with forms that mix numeric and non-numeric fields, the same pattern applies across the board. HTML nested forms introduce a similar set of browser-imposed constraints that are worth understanding before you build complex multi-field structures.




