A numeric text box is ready for production only when it behaves predictably for people, browsers, assistive technology, and your API. The fastest way to prove that is a repeatable test matrix, not a handful of happy-path checks.
In this guide, we use a quantity field as the running example: whole numbers from 1 to 99 in increments of 1. Adapt the same method for measurements, percentages, decimal amounts, and generated Vue forms. For a ready-made control with min, max, step, labels, descriptions, and validation state, start with our Vue Number Input, then use this matrix to verify the behavior your product actually needs.
What you will finish with: a set of tests that distinguishes raw editing text from committed numeric data, covers range and step rules, catches input-method regressions, and verifies that server errors do not discard a user’s work.
Table of contents
- Before you start: define the numeric contract
- 1. Model editing state separately from committed numeric state
- 2. Test bounds and zero as distinct cases
- 3. Verify step rules without floating-point surprises
- 4. Test formatting and locale behavior while the value is in progress
- 5. Exercise every input method, including mobile and paste
- 6. Check labels, descriptions, and error announcements
- 7. Prove client and server validation agree
- 8. Serialize schema-driven values intentionally
- A reusable numeric text box release checklist
- Ship a numeric text box that preserves trust
Before you start: define the numeric contract
Write the rules for one field before writing tests. A numeric text box cannot be tested well if the team has not agreed on what values mean.
For each field, document:
- Domain: quantity, money, measurement, percentage, or an identifier that only looks numeric.
- Allowed range: for example, 1 through 99.
- Increment:
1,0.25,0.01, or unrestricted. - Scale: maximum decimal places, if decimals are allowed.
- Empty-state rule: whether blank means
null, an empty string during editing, or a required-field error. - Canonical submission format: a JSON number, integer minor units, or a canonical decimal string.
- Input behavior: whether browser stepping, custom buttons, mouse wheel changes, and locale-aware formatting are intentional.
For bounded quantities, native number inputs provide built-in constraint validation for min, max, and step. The default step for number inputs is 1, and step validity is calculated from the configured base, commonly min. MDN’s number-input reference is a useful baseline for aligning browser behavior with your test expectations.
For formatted prices, do not force the same model onto the field. Our Money Input keeps the amount available as a string or number and lets the application own decimal transformation and billing policy. That separation is useful when a raw value is still being edited.
1. Model editing state separately from committed numeric state
Action: Identify four states in your field contract and test them individually:
- Empty:
''while editing, thennullor a required-field error when validation runs. - Incomplete: text that could become valid, such as
-,12., or0,in a supported locale. - Valid: text that parses and satisfies range, scale, and step rules.
- Invalid: malformed text or a complete number that breaks a domain rule.
Do not turn an incomplete state into invalid data or eagerly reformat it. A person typing 12. is not necessarily finished. If the UI rewrites it to 12 or 12.00 during entry, the caret can jump and the next keystroke can produce an unintended value.

Expected result: the raw display string remains available until a deliberate validation boundary, usually blur, submit, or an explicit commit action. The form model contains only a canonical value that passed your policy.
Troubleshooting: A common failure is treating Number(raw) as the complete parsing policy. It collapses distinctions you need to preserve. For example, Number('') becomes 0, which can silently convert an intentionally blank field into a valid-looking quantity. Keep the raw string and canonical value separate.
A compact state contract might look like this:
type NumericFieldState = {
raw: string
committed: number | string | null
status: 'empty' | 'incomplete' | 'valid' | 'invalid'
error: string | null
}
function classifyQuantity(raw: string): NumericFieldState {
if (raw === '') return { raw, committed: null, status: 'empty', error: null }
if (/^\d+$/.test(raw)) {
const value = Number(raw)
if (value >= 1 && value <= 99) {
return { raw, committed: value, status: 'valid', error: null }
}
return { raw, committed: null, status: 'invalid', error: 'Enter a value from 1 to 99.' }
}
return { raw, committed: null, status: 'invalid', error: 'Enter a whole number.' }
}
For decimal or formatted fields, make the incomplete branch explicit before attempting a numeric conversion. We recommend committing only after syntax and domain rules succeed.
2. Test bounds and zero as distinct cases
Action: Test every boundary, not just one value in the middle of the permitted range.
For a min: 1, max: 99, step: 1 quantity, run these cases through typing, paste, blur, submit, and any increment or decrement control:
| Input | Expected field result | Expected serialized value |
|---|---|---|
| empty | Required error only if the field is required | null or omitted, by contract |
0 |
Range error | No committed value |
1 |
Valid minimum | 1 |
50 |
Valid | 50 |
99 |
Valid maximum | 99 |
100 |
Range error | No committed value |
-1 |
Range or syntax error, as specified | No committed value |
001 |
Decide whether to preserve, normalize, or reject | Documented canonical form |
Expected result: a valid edge value remains valid after blur and submission. An out-of-range value remains visible with a specific correction message, rather than being silently clamped or cleared.
Troubleshooting: Silent clamping can be appropriate only when the product clearly communicates it and the changed value is safe. In most transactional, pricing, or configuration flows, it is better to preserve 100, explain the maximum, and let the person correct it.
Test zero separately even when negative values are already covered. Zero is often a meaningful value for stock, discount, duration, or allocation, but it is also commonly excluded from quantities. Your schema needs to express that product decision instead of relying on JavaScript truthiness.
3. Verify step rules without floating-point surprises
Action: Test whole-number and decimal stepping from the same declared step base.
The browser considers a typed value invalid when it does not meet the step configuration, even if the number is otherwise within range. For example, min="0" step="0.25" accepts 0, 0.25, and 0.5, while 0.3 is not on the quarter-step grid. Native controls may still allow someone to type an off-step value, so validate it on blur and submit as well as through spinner controls. MDN documents this constraint-validation behavior and the role of the step base.
Use this set for a min: 0, max: 10, step: 0.25 field:
0,0.25,9.75, and10should be valid.0.1,0.3, and9.99should produce a useful step error.- Values immediately below or above the range should produce range errors before you report a step error.
- Repeated increment and decrement actions should never create a displayed value such as
0.30000000000000004.
When decimal precision matters, compare scaled integers or canonical decimal strings rather than relying on binary floating-point equality. For quarter steps, multiply by 100 and compare integer cents, or use a decimal library in the application layer. The test should assert the submitted representation, not only the text in the field.
Expected result: your field explains the exact permitted increment, and the client and server agree on the same calculation.
Troubleshooting: Do not test only spinner clicks. Paste 0.3, edit an existing value in the middle, and submit a handcrafted off-step value to the API. Each route must reach the same rule.
4. Test formatting and locale behavior while the value is in progress
Action: Run display and parsing tests separately for every locale and format your product supports.
A numeric keyboard hint improves entry on many mobile devices, but it is not validation. inputmode="decimal" asks for a fractional numeric keyboard and may present the decimal separator for the user’s locale, while devices can differ in whether they offer a minus key. MDN describes inputmode as a hint, not a parser or security control.
For each supported locale, test:
- A raw decimal such as
12.5or12,5. - Grouped display values such as
1,234.50or1 234,50. - Leading and trailing whitespace.
- A pasted currency symbol or grouping separator.
- A trailing decimal separator while focused.
- Focus, blur, refocus, correction, and resubmission.
Decide whether formatted input is accepted directly or only displayed after a valid value loses focus. In either design, do not submit the visible currency string to the backend. Submit the canonical format defined in your field contract, such as a decimal string with two places or integer minor units.
Expected result: formatting makes the field easier to read without corrupting the in-progress input or changing its meaning.
Troubleshooting: Avoid global replacements such as “remove every comma and currency symbol.” That can misread valid input across locales. Define supported locale rules, test them, and reject formats you do not support with a clear message.
5. Exercise every input method, including mobile and paste
Action: Test the same contract with keyboard, pointer, paste, spin controls, wheel input, and mobile keyboards.
Use this execution checklist:
- Type digits, decimal separators, minus signs, and invalid letters.
- Paste valid, invalid, formatted, oversized, and whitespace-padded values.
- Use Arrow Up and Arrow Down when the control is incrementable.
- Test Home, End, Page Up, and Page Down only if your custom spinner implements them.
- Confirm whether wheel events can change a focused value, then test that choice deliberately.
- Test touch targets and numeric keyboards on representative iOS and Android devices.
- Ensure browser autofill or password-manager behavior cannot create an unvalidated committed value.
If you build a custom spinbutton, keyboard behavior is not optional. The W3C spinbutton pattern specifies direct text entry and describes expected Arrow, Home, End, and optional Page Up/Page Down behavior. It also requires a programmatic value, minimum, maximum, and invalid state where applicable. Review the WAI-ARIA spinbutton pattern before replacing native behavior.
Expected result: every supported interaction produces the same committed value or the same actionable error. No path bypasses parsing and validation.
Troubleshooting: Mouse wheel changes are especially easy to miss. If your product does not intend scroll-wheel editing, test that scrolling over a focused number field cannot accidentally alter a value. If it does support wheel changes, make the change visible and reversible.
6. Check labels, descriptions, and error announcements
Action: Test the numeric text box with a keyboard and at least one screen reader in the browsers you support.
Every field needs a visible, programmatically associated label. W3C recommends using a <label> and connecting its for value to the control’s id; the association gives assistive technology the control’s purpose and expands the clickable label area. See W3C’s form-label guidance.

For each validation state, verify:
- The label states what the number represents, for example, “Seats” rather than “Value.”
- Help text explains units, allowed range, and increments before an error occurs.
aria-invalidis applied only for the current invalid state.- The error is connected to the field and explains how to fix the value.
- A failed submit presents an error summary and moves focus predictably to the summary or first invalid field.
- Increment and decrement controls have accessible names and do not create an unexpected Tab sequence.
A custom widget has to provide the keyboard behavior native HTML normally supplies. W3C’s authoring guidance emphasizes that ARIA roles alone do not implement interaction, so test with the actual keyboard commands your component exposes.
Expected result: a person can identify the field, discover its numeric rules, find an error, correct it, and continue without relying on color or a pointer.
Troubleshooting: Do not announce a harsh error for every transient keystroke. An incomplete decimal may be a normal editing state. Give immediate feedback only when it helps, then revalidate on blur and submit.
7. Prove client and server validation agree
Action: Send valid and invalid payloads directly to the server, not just through the browser UI.
Client-side validation improves completion, but it cannot establish trust. The browser can be modified, requests can be replayed, and integrations can call your API without rendering the field. Frontend validation should help users enter valid data, while the backend repeats syntax, range, scale, step, authorization, and business-rule checks. web.dev’s form-validation guidance makes the same distinction between front-end validation and broader validation requirements.
Use an API test set that mirrors the UI matrix:
[
{ "quantity": null },
{ "quantity": "" },
{ "quantity": 0 },
{ "quantity": 1 },
{ "quantity": 99 },
{ "quantity": 100 },
{ "quantity": "12." },
{ "quantity": "five" },
{ "quantity": 1.5 }
]
For each rejected payload, assert three things:
- The API returns a stable field key and a plain-language message.
- The UI maps that error back to the correct field.
- The raw entered value stays visible so the person can correct it.
Expected result: a server rejection does not reset the whole form or replace the numeric text box with a generic toast.
Troubleshooting: Make error mapping part of your end-to-end test. It is not enough for the API to return a 400 response. The person must see the error beside the correct input after the response arrives.
8. Serialize schema-driven values intentionally
Action: Add serialization assertions to generated-form tests.
Schema-driven forms need an explicit boundary between field editing state and the submitted schema value. A number field may expose native constraints, but your generated layout still needs rules for empty values, partial strings, invalid entries, and canonical commits.
For a quantity, we recommend tests like these:
expect(serialize({ raw: '', committed: null })).toEqual({ quantity: null })
expect(serialize({ raw: '3', committed: 3 })).toEqual({ quantity: 3 })
expect(serialize({ raw: '3.', committed: null })).toThrow('Quantity is incomplete')
expect(serialize({ raw: '100', committed: null })).toThrow('Quantity must be at most 99')
The field configuration should be portable across generated layouts: range rules remain domain metadata, while UI configuration defines the label, description, and step interaction. In DOM Studio, the Number Input documentation shows a Vue v-model number with label, description, min, max, and step props. Use the live playground to vary those constraints, then run the same matrix against the form generated from your schema.
Expected result: changing a generated layout does not change the value contract or accidentally serialize an invalid raw string.
Troubleshooting: A currency field often needs a different canonical model than a quantity. Keep price values as canonical decimal strings or minor units when your domain needs exact scale, rather than treating every numeric-looking value as a JavaScript number.
A reusable numeric text box release checklist
Before shipping, verify that your test suite covers:
- Empty, incomplete, valid, and invalid raw states.
- Minimum, maximum, zero, negative, and just-outside-range values.
- Integer and decimal step rules, including paste and repeated incrementing.
- Focus, blur, reformatting, and locale-specific decimal and grouping behavior.
- Keyboard, pointer, wheel, spinner, touch, and mobile keyboard interactions.
- Visible labels, instructions, field-level errors, focus behavior, and assistive-technology feedback.
- Direct API rejection tests and field-level server-error mapping.
- Serialization of
null, canonical numeric values, and blocked invalid states.
For a short companion refresher on client-side form validation concepts, watch the verified JavaScript Client-side Form Validation video. Use it alongside this matrix, not in place of server-side tests.
Ship a numeric text box that preserves trust
A reliable numeric text box does not merely reject letters. It makes the field’s meaning explicit, preserves normal in-progress editing, validates the same rules across every interaction, and gives people a clear path to recovery when the server says no.
We recommend starting with one high-value numeric field, adding this matrix to component and end-to-end tests, then reusing the contract across your generated forms. When you are ready to implement or inspect the control itself, explore the DOM Studio Number Input to test min, max, step, labels, descriptions, and invalid state live.
