← All writing
Craft · · 8 min

Scroll wheels and zip codes

On the field that edits itself when you scroll past it, the leading zero it eats, and why the rest of the list goes unused.

Forms HTML

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, the customer had a screenshot of their cart with 4 in it, and the two of those sat there refusing to be reconciled for most of a morning while everybody typed quantities into the form and watched them stay put.

They typed 4, and then scrolled down the page to find the checkout button with the cursor still sitting over the quantity field, which still had focus.

A focused type="number" input treats the scroll wheel as a spinner.

That’s the whole thing. No keystroke, nothing in the logs that looks like an edit, and nothing that a person on a trackpad being careful about reproducing a support ticket is ever going to trigger by accident. I’d read about that behavior years ago and filed it under trivia, which 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. Things you would add up, average, or put a unit after. How many, how much, a price, a weight. For those it’s the right answer, and the persnickety little spinner arrows nobody has ever clicked on purpose at least make sense.

It’s wrong for a string of digits that merely looks like a number, which is most of the digits on a form. Zip codes, card numbers, phone numbers, OTP codes, account numbers, house numbers, anything with a leading zero that means something. The test I’ve used 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 in the same week. Somebody in Massachusetts types 02134 and 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 last month.

zip.html
<!-- not this -->
<input type="number" name="zip">

<!-- this -->
<input type="text" inputmode="numeric" pattern="[0-9]*" name="zip"
       autocomplete="postal-code">
HTML

inputmode gets you the numeric keypad on a phone, which is the only thing anybody wanted out of type="number" in these cases anyway. No spinner, no scroll wheel, no leading zero to argue about, and it’s a string the whole way through. The validation half of this is the same trade in the other direction: 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, and the two of them together is the most common configuration I see. Several tens of kilobytes of JavaScript calendar on the field the browser 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, and no 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 an entire 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 distance.

The caveats are real, so: 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, and that one is a genuine reason to reach for a component instead of a lazy one.

And then there are the ones that just sit there unspent. type="search" for a clear button and a keyboard whose action key says Search. type="range" for a slider that is 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, which opens the camera directly, and turns a “photograph your meter reading” flow from eight taps into two. type="color", which is a whole color picker on a project that was never going to budget for a color picker.

Because it doesn’t match the design

I don’t think anybody skips these out of ignorance, mostly. My sense is they skip them because they can’t be styled, and a form where 11 fields match the design system and the 12th looks like whatever Chrome feels like today reads as unfinished. The designer 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.

I’d push 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 is available. Native controls are at their best on phones and at their most visually inconsistent on desktop, which is a convenient shape to be handed: the custom widget can be a large-viewport enhancement sitting on top of a native base. I’ve built it that way twice, it was less work than the full custom version both times, and it’s 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> is the most under-used thing in HTML and I’d like more people to know it exists.

branch.html
<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>
HTML

That’s an autocomplete. It filters as you type, it’s keyboard navigable, it’s announced correctly, and it costs nothing. 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 suggests instead of restricting, so somebody can type a value that isn’t in the list. For “which of our 11 branches” that second part is 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, because 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 and that’s the single field on the form where the type was right the whole time. It just also needed a line of JavaScript to stop the browser editing it for free 🥲

quantity.js
// a focused number input treats the wheel as a spinner
qty.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
JavaScript

Which is a deflating place to end up. The rule held, the field that cost the client an order had been following the rule 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 browser trackers for years and I don’t expect it to move. So it goes in the notes with the rest of what I only know because it cost somebody money. Mostly the platform is better than what I’d write myself, occasionally it’s carrying a decision from 2011 that nobody has been willing to revisit, and the actual work is telling the two apart before a customer does it for you.

Read similar posts
2 min

fieldset and legend

A group of radio buttons has a question attached to it, and the question needs to be in the markup rather than in a heading that happens to sit nearby.

1 min

optgroup

A select with 40 options in it is nearly always a select that wanted three or four labeled groups, and the element for that has been sitting in HTML the entire time.