HTMLNumber & Range Inputs

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

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

min

The smallest acceptable value

max

The largest acceptable value

step

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

HTML
<label for="volume">Volume</label>
<input type="range" id="volume" name="volume" min="0" max="100" step="1" value="50">
range has no visible numeric readout by default
Unlike 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

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

Leading zeros are the classic bug
Typing 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

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

type="number"

Yes

No — stripped

Yes

type="text" + inputmode="numeric" + pattern

No

Yes

Yes

Rule of thumb
If the field will ever be used in arithmetic (quantity, price, age), 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 by min/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" with pattern for validation and inputmode="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

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

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

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

1 (default)

Only whole numbers are valid

0.01

Two decimal places, e.g. for currency

5

Only multiples of 5 from the min are valid

any

Any decimal value is accepted, no increment restriction

step-examples.html

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">
Mismatched step causes silent validation failures
If a value doesn't align with 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.