Number and Range Inputs
type="number" and type="range" both collect numeric values, but they're built for different situations: number is for entering a precise value, and range is a slider for picking an approximate value visually. Neither is a good fit for every "numeric-looking" field — knowing when to reach for plain text instead is just as important.
input type="number"
type="number" renders a text field with up/down spinner arrows, restricting input to numeric values. min, max, and step control the valid range and increment.
number-basic.html
<label for="quantity">Quantity</label> <input type="number" id="quantity" name="quantity" min="1" max="10" step="1" value="1"> <label for="price">Price ($)</label> <input type="number" id="price" name="price" min="0" step="0.01">
Attribute | Purpose |
|---|---|
| The smallest acceptable value |
| The largest acceptable value |
| The increment the spinner arrows move by, and what valid values must be a multiple of |
input type="range"
type="range" renders a draggable slider instead of a text field. It's meant for cases where the exact number matters less than picking a value quickly within a known range — volume, brightness, a price filter threshold.
range-basic.html
<label for="volume">Volume</label> <input type="range" id="volume" name="volume" min="0" max="100" step="1" value="50">
number, a range slider shows no text value on its own — users just see the handle's position. If the exact number matters to your users, pair it with a live label updated via JavaScript, or use <output>.range-with-output.html
<label for="volume">Volume</label>
<input type="range" id="volume" name="volume" min="0" max="100"
value="50" oninput="volumeLabel.value = volume.value">
<output id="volumeLabel" for="volume">50</output>When type="number" Causes UX Problems
type="number" is easy to reach for whenever a field only accepts digits, but several common fields look numeric while behaving nothing like a math value — and forcing them into a number input creates real usability problems.
Field | Problem with type="number" |
|---|---|
Phone numbers | Spinner arrows are meaningless, and leading formatting like "+1" or parentheses isn't valid numeric syntax |
ZIP / postal codes | Codes with leading zeros (e.g. "00501") get silently stripped, since 00501 and 501 are the same number to a number input |
Credit card numbers | Far too long for a spinner control, and grouping/formatting isn't numeric-safe |
Account or reference numbers | Often contain leading zeros or letters mixed with digits, which a number input can't represent at all |
007 into a type="number" field is stored and submitted as 7 — the leading zeros are simply not part of a numeric value. For any field where leading zeros are meaningful (postal codes, ID numbers), a number input will silently corrupt the data.The Fix: text + pattern + inputmode
For these "numeric-looking but not actually a math value" fields, use type="text" with a pattern for validation and inputmode="numeric" to still get a numeric mobile keyboard — without any of the number input's side effects like spinner arrows or leading-zero stripping.
text-pattern-fix.html
<!-- Postal code: preserves leading zeros, no spinner arrows -->
<label for="zip">ZIP code</label>
<input
type="text"
id="zip"
name="zip"
inputmode="numeric"
pattern="[0-9]{5}"
maxlength="5"
>
<!-- Phone number: numeric keypad without number-input semantics -->
<label for="phone">Phone number</label>
<input type="tel" id="phone" name="phone" inputmode="tel">Approach | Spinner arrows | Leading zeros preserved | Numeric mobile keyboard |
|---|---|---|---|
| Yes | No — stripped | Yes |
| No | Yes | Yes |
type="number" is right. If it's really an identifier that merely looks like digits (ZIP code, phone number, card number), use text with pattern and inputmode instead.Use
type="number"for values you'll actually do math with, constrained bymin/max/step.Use
type="range"for approximate values picked visually, and pair it with<output>if the exact number matters.Avoid
type="number"for phone numbers, postal codes, or IDs — leading zeros get stripped and spinner arrows make no sense.For numeric-looking identifiers, use
type="text"withpatternfor validation andinputmode="numeric"for the mobile keyboard.
Reading Numeric Values in JavaScript
Both number and range inputs expose a special valueAsNumber property that returns an actual JavaScript number (or NaN if empty/invalid), saving you a manual parseFloat call on the string value.
value-as-number.js
const quantityInput = document.getElementById('quantity');
console.log(quantityInput.value); // "3" — a string
console.log(quantityInput.valueAsNumber); // 3 — an actual number
quantityInput.addEventListener('input', () => {
const total = quantityInput.valueAsNumber * 19.99;
console.log(total.toFixed(2));
});Multiple Range Sliders for a Min/Max Filter
A common e-commerce pattern uses two overlapping range inputs to let users pick a price range, keeping the lower slider from exceeding the upper one via a small script.
dual-range.html
<label for="min-price">Min price</label> <input type="range" id="min-price" min="0" max="500" value="0"> <label for="max-price">Max price</label> <input type="range" id="max-price" min="0" max="500" value="500">
dual-range.js
const minPrice = document.getElementById('min-price');
const maxPrice = document.getElementById('max-price');
minPrice.addEventListener('input', () => {
if (minPrice.valueAsNumber > maxPrice.valueAsNumber) {
minPrice.value = maxPrice.value;
}
});
maxPrice.addEventListener('input', () => {
if (maxPrice.valueAsNumber < minPrice.valueAsNumber) {
maxPrice.value = minPrice.value;
}
});The step Attribute and Precision
step controls both the spinner/slider increment and what counts as a valid value. step="any" disables the increment restriction entirely, allowing arbitrary decimal precision.
step value | Effect |
|---|---|
| Only whole numbers are valid |
| Two decimal places, e.g. for currency |
| Only multiples of 5 from the |
| Any decimal value is accepted, no increment restriction |
step-examples.html
<!-- Currency: two decimal places --> <input type="number" name="price" min="0" step="0.01"> <!-- Any precision allowed --> <input type="number" name="measurement" step="any">
step (e.g. typing 19.999 when step="0.01"), the field fails native validation even though it looks like a reasonable number. Set step deliberately to match the real precision you expect, or use step="any" when arbitrary precision is genuinely fine.