Build a credit-card field with input[type=number] and the value changes when someone scrolls the wheel over it. Type the wrong number of digits and nothing on screen explains what went wrong. I had been reaching for the element without thinking about what it means, so here is where I draw the line around it.
input[type=number] is a spin button
Use input[type=number] when incrementing or decrementing a numeric value is useful. MDN likewise recommends it for incremental numbers rather than any string that happens to contain digits.
The element has an implicit spinbutton role. It is a numeric value that can be stepped within a range, not a field that accepts digits.
For a quantity such as the number of products in an order, start with the native input[type=number] and its min, max, and step attributes. Replacing it with a custom role="spinbutton" just to change its appearance means recreating keyboard behavior, value updates, and assistive-technology announcements yourself.
Values made of digits are not always numbers
A credit-card number or postal code contains digits, but it is not a number used in a calculation. For these values, use type="text" and consider inputmode="numeric" to request a numeric virtual keyboard. A pattern can express constraints such as the required number of digits.
Do not rely on the title attribute alone to explain those constraints. Show the guidance as visible text and associate it with the input through aria-describedby, so people can understand the requirement before submitting the form.
<form>
<label for="digit">Verification code</label>
<input
id="digit"
name="digit"
type="text"
inputmode="numeric"
pattern="\d{3}"
aria-describedby="digit-description" />
<span id="digit-description">Enter a three-digit number.</span>
<button>Submit</button>
</form>
For a custom validation message, connect the input and message with aria-describedby, expose the invalid state with aria-invalid, and consider role="status" or aria-live when a message is inserted dynamically after submission.
<label for="digit-with-error">Verification code</label>
<input
id="digit-with-error"
name="digit"
type="text"
inputmode="numeric"
pattern="\d{3}"
aria-invalid="true"
aria-describedby="digit-error" />
<p id="digit-error" role="status" aria-live="polite">
Enter a three-digit number.
</p>
The question to ask is whether the value is a number and whether stepping through it helps the person filling in the form. Looking numeric is not a reason. The problem was not that input[type=number] was difficult to use; I had chosen it for a use case it was not designed to handle.
Choosing input[type=date]
input[type=date] submits either an empty string or a date string in yyyy-mm-dd format, regardless of how the control displays the date. Its UI and interaction can vary across browsers and operating systems.
Start by considering the native input, which gives the browser responsibility for localization and keyboard interaction. Reach for a date-picker component when the native control cannot meet a real requirement, such as restricting a complex set of dates or selecting a range. A custom date picker is difficult to make accessible, so appearance alone is not a good reason to replace the native control.
Closing note
Familiar elements are the easy ones to use without revisiting their purpose. Pick form controls by what the value means and how people will interact with it, and leave appearance out of the decision.