Scroll wheels and zip codes
A number input edits itself when you scroll past it, and most of the digits on a form were never numbers in the first place.
A client forwarded me a support email about an order for 40 of a thing the customer was certain they had ordered 4 of.
Nobody could reproduce it. The order record said 40 and the customer had a screenshot of their cart with 4 in it. The two sat there refusing to be reconciled for most of a morning while everybody typed quantities into the form and watched them stay put.
The customer had typed 4 and then scrolled down the page to find the checkout button, with the cursor still sitting over the quantity field and the field still focused.
A focused type="number" input treats the scroll wheel as a spinner.
That’s the whole thing. There’s no keystroke and nothing in the logs that looks like an edit. And nobody on a trackpad who’s carefully trying to reproduce a support ticket is ever going to trigger it by accident. I’d read about that behavior years ago and filed it under trivia, which I guess is what a browser quirk is right up until it turns up on an invoice.
Is a zip code a number?
type="number" is for quantities. It’s for things you’d add up or average or put a unit after, like a count or a price or a weight. For those it’s the right answer, and even the fiddly little spinner arrows nobody has ever clicked on purpose at least make sense.
It’s wrong for a string of digits that just happens to look like a number, which is most of the digits on a form. Zip codes, card numbers, phone numbers, OTP codes, account numbers, house numbers, and anything with a leading zero that means something. I’ve had a test for this since that morning. If adding one to it is nonsense, it isn’t a number.
The same form had the zip code as type="number" too, which is how the leading zero problem came up the same week. Say somebody in Massachusetts types 02134. Whether that zero survives depends on the browser, then on your form handler, and then on whether the column it lands in is an integer.
It only has to lose that argument once. There’s no error anywhere in that chain. The value just gets quietly smaller, which puts it in the same family as the nesting rules that stopped being rules in April.
<!-- not this -->
<input type="number" name="zip">
<!-- this -->
<input type="text" inputmode="numeric" pattern="[0-9]*" name="zip"
autocomplete="postal-code">
inputmode gets you the numeric keypad on a phone, which is the only thing anybody wanted out of type="number" in these cases anyway. There’s no spinner, no scroll wheel and no leading zero to argue about. It’s a string the whole way through.
The validation half of this is the same trade in the other direction. You take the part of the built-in behavior you want and leave the part you don’t.
The rest of the list is free
That form also had the phone number as type="text", which is the mistake in the other direction. The two together are the most common configuration I see. That usually means several tens of kilobytes of JavaScript calendar on the field the browser already handles beautifully and a bare text input on the field where one word would have done it. Backwards af.
type="tel" gives you a numeric keypad with the big, well-spaced keys people already use for dialing. It doesn’t do any numeric validation, which is correct because a phone number isn’t a number either. It has spaces and parentheses and a plus sign and a leading zero that matters enormously.
The date family (date, time, datetime-local, month, week) buys three things that are hard to get any other way. The displayed format follows the user’s locale, so somebody who writes the day first sees the day first without you doing anything about it or getting it wrong. The value your server receives is ISO regardless of what was displayed, which deletes a whole category of parsing bug. And on a phone the native picker is a big, familiar, well-tested control that beats every JavaScript calendar I’ve used on a small screen by a mile.
The caveats are real, though. You can’t restyle them much. The picker chrome belongs to the browser and every browser’s is different. There’s no way around that.
min, max and step work. “Disable these specific dates” doesn’t. That one is a genuine reason to reach for a component, not a lazy one.
And then there are the ones that just sit there unspent. type="search" gets you a clear button and a keyboard whose action key says Search. type="range" is a slider that’s already keyboard operable, arrows and Home and End and all, which is more than I can say for any custom slider I’ve ever tested.
type="file" with capture opens the camera directly, which turns a “photograph your meter reading” flow from eight taps into two. type="color" is a whole color picker on a project that was never going to budget for one.
Because it doesn’t match the design
I don’t think anybody skips these out of ignorance, mostly. I feel like they skip them because they can’t be styled. A form where 11 fields match the design system and the 12th looks like whatever Chrome feels like today reads as unfinished.
Whoever designed the form spots it before anyone else does and they’re not wrong about what they’re looking at. That’s a legitimate constraint and I’m not going to pretend it away.
Still, I’d push back on the conclusion because the trade isn’t “custom picker or native picker.”
It’s (1) a custom picker, plus its focus handling, plus its keyboard support, plus whatever it announces to a screenreader, plus ~30 KB, plus being worse on the device most of your visitors are actually holding, against (2) a control that looks slightly different in Firefox.
There’s also a middle version that people forget about. Native controls are at their best on phones and at their most visually inconsistent on desktop, which is kind of a convenient split. It means the custom widget can be a large-viewport enhancement sitting on top of a native base.
I’ve built it that way twice and it was less work than the full custom version both times. That version is exactly the kind of thing that turns progressive enhancement into a claim somebody could check instead of a line in a proposal.
Seven lines of combobox
While I’m here, <datalist> might be the most under-used thing in HTML and I’d like more people to know it exists.
<label for="branch">Nearest branch</label>
<input id="branch" name="branch" list="branches" autocomplete="off">
<datalist id="branches">
<option value="San Diego">
<option value="Los Angeles">
<option value="Sacramento">
</datalist>
That’s an autocomplete. It filters as you type and it’s keyboard navigable and announced correctly. It also costs nothing. It’s the combobox pattern people pull in a library for and then get wrong, in seven lines of markup.
Its limits are worth knowing before you get attached. You can’t style the dropdown at all (read: your designer will ask about it in the first review).
It also suggests instead of restricting, so somebody can type a value that isn’t in the list. For “which of our 11 branches” that’s a bug and you’d be validating server-side anyway. For “what’s your job title,” it’s the feature.
What we changed
Zip became a text input with inputmode. Phone became tel. The date field lost its picker on phones and kept it on desktop. The client wanted their calendar and I’d rather spend the argument on the half of the traffic it hurts.
And the quantity field, the one that started all of this, stayed type="number" because a quantity is a quantity. It’s the one field on the form where the type was right the whole time. It just also needed a line of JavaScript to stop the browser from editing it for free 😅
// a focused number input treats the wheel as a spinner
qty.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
Which is a deflating place to end up. The rule held. The field that cost the client an order had been following it the whole time, and the fix was the thing I spend most of my time talking people out of.
That wheel behavior has been sitting in the browsers’ bug trackers for years and I don’t expect it to move. So it goes in the notes with the rest of the stuff I only know because it cost somebody money.
Mostly the platform is better than what I’d write myself. Every so often it’s carrying a decision from 2011 that nobody has been willing to revisit. I think most of the job is telling those two apart before a customer does it for you.