React Form with Number Input
Custom number inputs with increment and decrement buttons solve one of the biggest UX complaints about native number inputs — the tiny spin arrows that are nearly impossible to tap on mobile. Clamping the value between a minimum and maximum on every change, rather than only on blur, prevents invalid states from being submitted or displayed.
The challenge
Number inputs require validation, clamping, and formatting that the native input does not fully handle, especially custom increment/decrement controls.
- Clamping the value between min and max when the user types freely into the input field
- Preventing non-numeric characters from being entered or pasted into the field
- Disabling the decrement button at the minimum value and the increment button at the maximum
- Handling the empty string state — when the user clears the field, the value should not flash to 0
Working code example
Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.
import { useState } from 'react';
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
export default function NumberInputForm() {
const [qty, setQty] = useState(1);
const [amount, setAmount] = useState('');
const [submitted, setSubmitted] = useState(false);
const changeQty = (delta) => setQty((q) => clamp(q + delta, 1, 99));
const handleAmountChange = (e) => {
const raw = e.target.value;
if (raw === '' || raw === '-') { setAmount(raw); return; }
const num = parseFloat(raw);
if (!isNaN(num)) setAmount(raw);
};
const handleAmountBlur = () => {
const num = parseFloat(amount);
if (isNaN(num)) { setAmount(''); return; }
setAmount(String(clamp(num, 0, 10000)));
};
const handleSubmit = (e) => {
e.preventDefault();
if (!amount || isNaN(parseFloat(amount))) { alert('Please enter a valid amount'); return; }
setSubmitted(true);
};
if (submitted)
return (
<div className="max-w-sm mx-auto p-6 bg-green-50 rounded-2xl text-green-700">
<p className="font-semibold">Order placed!</p>
<p className="text-sm mt-1">Quantity: {qty} · Amount: ${parseFloat(amount).toFixed(2)}</p>
</div>
);
return (
<form onSubmit={handleSubmit} className="max-w-sm mx-auto p-6 bg-white rounded-2xl shadow space-y-5">
<h2 className="text-xl font-bold text-gray-800">Place Order</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Quantity (1–99)</label>
<div className="flex items-center gap-3">
<button type="button" onClick={() => changeQty(-1)} disabled={qty <= 1}
className="w-9 h-9 rounded-lg border border-gray-300 text-lg font-bold text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition">
−
</button>
<span className="w-12 text-center font-mono font-semibold text-gray-800">{qty}</span>
<button type="button" onClick={() => changeQty(1)} disabled={qty >= 99}
className="w-9 h-9 rounded-lg border border-gray-300 text-lg font-bold text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition">
+
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Amount ($0–$10,000)</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<input
type="number"
value={amount}
onChange={handleAmountChange}
onBlur={handleAmountBlur}
min={0}
max={10000}
step={0.01}
placeholder="0.00"
className="w-full border border-gray-300 rounded-lg pl-7 pr-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
</div>
<button type="submit"
className="w-full bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Place Order
</button>
</form>
);
}How ReactForm.co helps
ReactForm's visual builder handles all of the above — number input configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.
Build this form visuallyFrequently asked questions
How do I clamp a number input to a min/max range in React?
Read the value on change, parse it with parseInt or parseFloat, then clamp: Math.min(Math.max(value, min), max). For a better UX, only clamp on blur so users can type freely. On change, just update state with the raw string and validate on blur or submit. This prevents the input from jumping while the user is typing.
How do I build custom increment/decrement buttons for a number input?
Store the value in state as a number. Use two buttons with onClick handlers that add or subtract 1 (or your step value) and clamp the result. Set disabled={value <= min} on the decrement button and disabled={value >= max} on the increment button. This provides better mobile UX than the native spinner arrows.
How do I prevent letters from being typed into a number input?
The type="number" attribute prevents most non-numeric input in modern browsers, but users can still paste invalid values. In your onChange handler, check that the parsed value is not NaN before setting state. You can also add an onKeyDown handler that calls e.preventDefault() for non-numeric keys, but this often blocks legitimate inputs like backspace.
Related topics
Build this form visually — no code needed
ReactForm.co handles number input fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.

