When Default HTML Inputs Fail: Building a Medical-Grade Numeric Input in React

When developing healthcare applications, precision isn't just a nice-to-have—it's critical. This article explores how I discovered a subtle but dangerous flaw in the standard HTML number input and built a custom React solution that maintains data integrity for medical applications.
The Unexpected Problem
Copy
<input
type="number"
min="-20"
max="20"
step="0.25"
value={sphereValue}
onChange={(e) => setSphereValue(e.target.value)}
/>
During testing of an optometry system I built, I noticed something alarming: when doctors entered "+1.50" for a prescription, the native number input silently stripped the plus sign, storing just "1.50".
In most applications, this wouldn't matter. But in ophthalmology, the explicit plus sign has clinical significance—it represents a fundamentally different prescription. This silent data transformation could potentially lead to incorrect lenses being prescribed.
Why Standard Components Fall Short
The standard <input type="number">
has several limitations for medical applications:
It removes leading "+" signs automatically
It applies browser-specific formatting that can't be consistently controlled
It offers limited control over the validation process
It can introduce rounding errors with decimal values
For general use, these behaviors are reasonable. For medical data where precision matters, they're unacceptable.
The Solution: Character-Level Control
The solution was to build a custom component that treats numeric values as strings, giving complete control over character validation, formatting, and display.
Copy
const PrecisionNumericInput: React.FC<NumericInputProps> = ({
value,
onChange,
inputLabel,
errorCheck,
...rest
}) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Only allow digits, plus, minus, and decimal point
const rawValue = e.target.value?.replace(/[^0-9+-.]/g, "") || "";
if (onChange) onChange(rawValue);
};
const getError = () => {
if (!value) return false;
return errorCheck ? errorCheck(value) : false;
};
return (
<TextField
inputProps={{
step: 0.25,
inputMode: "numeric",
...rest.inputProps,
}}
type="text" // Using text type instead of number
value={value || ""}
onChange={handleChange}
error={getError()}
size="small"
label={inputLabel}
sx={{
"& .MuiInputBase-root": {
height: 32,
},
...rest.sx,
}}
InputLabelProps={{
shrink: true,
...rest.InputLabelProps,
}}
placeholder={inputLabel}
/>
);
};
Key Design Decisions
Using
type="text"
instead oftype="number"
This gives complete control over what characters are permitted and retainedCustom character filtering with regex The component explicitly filters input to only allow digits, signs, and decimal points
Explicit error handling The component accepts a custom validation function for domain-specific rules
Consistent visual styling Material UI's TextField ensures the component feels like a native input while providing more control
Validation Strategy: Trust Nothing
To ensure the component behaved correctly in all scenarios, I created a comprehensive test suite:
Copy
it("preserves leading plus signs", () => {
fireEvent.change(input, { target: { value: "+2.25" } });
expect(onChange).toHaveBeenCalledWith("+2.25");
});
it("handles negative values correctly", () => {
fireEvent.change(input, { target: { value: "-1.75" } });
expect(onChange).toHaveBeenCalledWith("-1.75");
});
it("strips non-numeric characters", () => {
fireEvent.change(input, { target: { value: "abc3.50xyz" } });
expect(onChange).toHaveBeenCalledWith("3.50");
});
it("allows decimal precision to two places", () => {
fireEvent.change(input, { target: { value: "2.75" } });
expect(onChange).toHaveBeenCalledWith("2.75");
});
This validation approach ensured the component would handle real-world clinical data entry scenarios reliably.
Integration with Form Libraries
The component works seamlessly with React Hook Form:
Copy
<Controller
name="sphere_right_eye"
control={control}
rules={{
validate: validateSphereValue
}}
render={({ field, fieldState }) => (
<PrecisionNumericInput
{...field}
inputLabel="SPH"
errorCheck={(val) => !!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
Performance Impact
Beyond the clinical benefits, this solution also improved performance in unexpected ways:
Eliminated unnecessary re-renders triggered by browser number formatting
Removed the need for complex state transformations between form submission and API calls
Simplified validation logic by working with consistent string representations
Broader Applications
This approach isn't limited to ophthalmology. Similar precision concerns exist in:
Medication dosing calculations
Laboratory result entry
Vital sign documentation
Financial calculations in healthcare billing
Lessons Learned
This experience reinforced several important principles for medical software development:
Never trust default form controls for clinical data
When precision matters, build custom components from the ground upValidate with domain experts
What seems like a minor UI quirk to developers may have significant clinical implicationsTest edge cases thoroughly
Especially scenarios involving decimal precision, leading zeros, and explicit signsDocument the rationale
Make sure future developers understand why custom components were necessary
Conclusion
Building high-quality medical software often means challenging standard web development practices. While the HTML specification has evolved significantly, there are still cases where the defaults don't align with clinical requirements.
By taking control at the character level, we can build numeric inputs that capture data precisely as clinicians enter it—preserving both the explicit and implicit meaning of the values. This extra effort pays dividends in data integrity, clinical safety, and maintaining the trust of healthcare professionals who depend on our applications.
Subscribe to my newsletter
Read articles from Lahiru Shiran directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
