A numericinput is reliable when it does more than reject letters. It needs a clear value contract, predictable editing behavior, accessible feedback, and the same validation rules in the browser and on the server.
For a bounded quantity such as items per order, we recommend defining the field once in your schema, mapping it into a Vue number input, validating after an intentional interaction boundary, and submitting only a canonical number. This guide walks through that process with a quantity field from 1 to 99.
What you will build: a Vue numeric input that accepts whole quantities, preserves empty and invalid states long enough for people to correct them, exposes its constraints clearly, and sends a validated value to your API.
Table of contents
- Before you start
- 1. Define the numeric field contract before choosing props
- 2. Model empty, editing, invalid, and committed values separately
- 3. Map the schema to a Vue numeric input
- 4. Treat range and step as validation rules, not input filters
- 5. Validate at the right time and make the correction accessible
- 6. Submit a canonical value and validate it again on the server
- 7. Reuse the contract across generated layouts
- Frequently asked questions
- Build a numeric input that stays predictable
Before you start
You need Vue 3, a field schema or comparable metadata object, and an API endpoint that can validate the same numeric rules. This example uses DOM Studio’s Vue DomNumberInput component, but the contract applies to any control that supports a model value, labels, descriptions, and numeric constraints.
Choose a native-style number input when people are entering an actual incrementable quantity. HTML number controls provide browser constraint validation for numeric values, and min, max, and step define the permitted range and increments. They are not a security boundary, so the server must repeat the checks before it trusts a submission. See MDN’s number-input reference for the browser behavior behind these constraints.
Use a different field pattern for values that only look numeric, such as postal codes, account identifiers, or highly formatted currency. This article focuses on the quantity case, where stepping and numeric range rules are meaningful.
1. Define the numeric field contract before choosing props
Start with the value your application needs, not with the UI control. A schema-driven field should answer these questions explicitly:
- Is the canonical value a number, a decimal string, or
null? - Is the field required?
- What is the inclusive minimum and maximum?
- Which increments are allowed?
- What does an empty field mean before validation?
- When should validation run?
- Is the field editable, read-only, or disabled?
For an order quantity, the contract can be small and unambiguous:
type QuantityField = {
kind: 'number'
required: true
min: 1
max: 99
step: 1
defaultValue: 1
validateOn: 'blur'
}
const quantityField: QuantityField = {
kind: 'number',
required: true,
min: 1,
max: 99,
step: 1,
defaultValue: 1,
validateOn: 'blur',
}
Expected result: anyone rendering this field, whether manually or from a generated layout, gets the same domain rules. The layout can change without changing what values the API accepts.
Troubleshooting: do not use truthiness to test whether a number exists. 0 is falsy in JavaScript, but it may be a valid value in domains such as allocation, discount, or duration. Treat required state as a separate rule.
2. Model empty, editing, invalid, and committed values separately
A numeric input does not move straight from blank to valid. During a real edit, it can be empty, partially entered, outside the allowed range, or valid and ready to commit.
For whole quantities, a simple state model is enough:
type QuantityState = {
raw: string
committed: number | null
status: 'empty' | 'editing' | 'invalid' | 'valid'
error: string | null
}
function validateQuantity(raw: string): QuantityState {
if (raw === '') {
return { raw, committed: null, status: 'empty', error: null }
}
if (!/^\d+$/.test(raw)) {
return { raw, committed: null, status: 'invalid', error: 'Enter a whole number.' }
}
const value = Number(raw)
if (value < 1 || value > 99) {
return { raw, committed: null, status: 'invalid', error: 'Enter a quantity from 1 to 99.' }
}
return { raw, committed: value, status: 'valid', error: null }
}
Keep the editable representation until a sensible commitment point, such as blur, submit, or an explicit save action. A required error belongs after validation runs, not necessarily the instant a user clears the field to replace its value.

Expected result: '' remains distinct from 0, 100 remains visible long enough to correct, and only a valid quantity becomes the committed form value.
Troubleshooting: Number('') produces 0, which can silently turn an intentionally blank field into a valid-looking value. Avoid conversion until your empty-state rule has been handled.
3. Map the schema to a Vue numeric input
The adapter between schema and component should be intentionally boring. Domain rules come from the schema. UI details, such as the visible label and help text, can live alongside the field definition or in a presentation adapter.
DOM Studio’s Vue Number Input provides a live playground for v-model, label, description, min, max, and step. We use it here because the component configuration mirrors the quantity contract directly.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DomNumberInput } from '@getdom/studio/vue'
const quantity = ref<number | null>(1)
const attemptedSubmit = ref(false)
const quantityError = computed(() => {
if (quantity.value === null) {
return attemptedSubmit.value ? 'Enter a quantity.' : ''
}
if (quantity.value < 1 || quantity.value > 99) {
return 'Enter a quantity from 1 to 99.'
}
return ''
})
function submit() {
attemptedSubmit.value = true
if (quantityError.value) return
// Send quantity.value to an API that validates the same contract.
}
</script>
<template>
<form @submit.prevent="submit">
<DomNumberInput
v-model="quantity"
label="Quantity"
description="Choose between 1 and 99 items."
:min="1"
:max="99"
:step="1"
:required="true"
:invalid="Boolean(quantityError)"
:errors="quantityError ? [quantityError] : []"
:validate-on-blur="true"
/>
<button type="submit">Save quantity</button>
</form>
</template>
Expected result: the input displays a meaningful label and range guidance, binds a numeric quantity, and applies an error state when your form policy says the value is invalid.
Troubleshooting: do not duplicate business rules in disconnected places. If the schema says the maximum is 99 but a component hard-codes 100, generated and hand-built layouts will eventually disagree. Map min, max, step, and required state from one source of truth.
4. Treat range and step as validation rules, not input filters
min, max, and step improve entry, but people can still type or paste a value that fails one of those rules. A number control may allow 100 to be entered even when its visible increment buttons stop at 99. Your application must decide how to respond.
We recommend preserving the entered value, marking it invalid on blur or submit, and explaining the correction. Silently clamping 100 to 99 can obscure a decision the user needs to make. For high-impact configuration or purchasing flows, preserving the original entry is usually safer and easier to audit.
The step rule is also more precise than “decimal values allowed.” MDN defines valid number-input steps relative to a step base, which is commonly min. With min="0" and step="0.25", 0.25 and 0.50 are valid increments while 0.30 fails step validation. Review MDN’s guidance on the step attribute when setting fractional rules.
For a schema adapter, use scaled integers or a decimal representation when fractional precision matters. For example, store cents or quarter-units when that maps cleanly to the domain. Do not rely on floating-point equality to decide whether a value is exactly on a decimal step.
Verification check: test the minimum, maximum, zero, one valid middle value, just-below-minimum, just-above-maximum, and a manually typed off-step value. Confirm each path produces the same committed value or the same field-level error.
5. Validate at the right time and make the correction accessible
Immediate feedback is helpful when it identifies a completed mistake. It is disruptive when it treats ordinary editing as failure. For a quantity field, validating on blur gives people room to replace a value, while submit validation ensures untouched required fields are still checked.
Every numeric field needs a visible label, concise instructions, and an error connected to the control. DOM Studio’s component supports label, description, invalid state, and errors in its configuration. For custom spinbuttons, the W3C quantity spinbutton example shows the additional work required for keyboard behavior, programmatic minimum and maximum values, and error association.
Apply these checks:
- Use a visible label that says what the number represents, such as “Quantity” or “Team seats.”
- Explain range and unit before an error occurs.
- Set invalid state only while the field currently has a correction to make.
- Put the correction beside the field and state how to fix it.
- Keep disabled and read-only distinct. Disabled fields are unavailable; read-only values remain relevant to review and form context.
- If you build custom increment buttons, give them accessible names and test keyboard and touch interaction.

Expected result: a keyboard or assistive-technology user can identify the field, discover the constraints, correct an error, and continue without guessing from color alone.
Troubleshooting: native semantics are usually less work than a custom spinbutton. If you replace a native interaction, you own the keyboard behavior, focus handling, announcements, and error semantics as well.
For a short refresher on Vue client-side validation concepts, this independent video is a useful companion. Apply its approach to your field contract rather than treating any client-side check as a replacement for API validation.
6. Submit a canonical value and validate it again on the server
The browser’s constraints are for usability, not trust. Requests can bypass your Vue UI, and client markup can be modified. Submit the committed canonical value and repeat syntax, range, step, authorization, and business validation on the server.
For the quantity contract, a small server validator might look like this:
function parseQuantity(value: unknown) {
if (typeof value !== 'number' || !Number.isInteger(value)) {
return { ok: false, error: 'Quantity must be a whole number.' }
}
if (value < 1 || value > 99) {
return { ok: false, error: 'Quantity must be from 1 to 99.' }
}
return { ok: true, value }
}
Return field-specific errors from the API, then map them back to the numeric input without clearing unrelated form values. This makes a rejected submission recoverable: the person sees the original value, understands the rule, and can correct the single field.
Verification check: send direct API requests with null, 0, 1, 99, 100, 1.5, and a string value. The server should accept only the values your schema defines as canonical.
7. Reuse the contract across generated layouts
Once the field contract is stable, it should survive changes in layout, labels, and presentation. That is the advantage of a schema-driven approach: generated forms can render the same numeric rule in a settings panel, onboarding flow, or admin screen without rewriting validation logic.
Keep the responsibilities separate:
- Schema: type, required state, range, step, default, and canonical value rules.
- Component adapter: maps schema constraints into
min,max,step, model updates, and validation timing. - Presentation: label, description, placement, and whether the field is disabled or read-only in a particular workflow.
- Server: authoritative validation and business permissions.
If your team needs to inspect the component configuration in a real Vue control, open the DOM Studio Number Input playground. Use it to vary range, step, labels, and states, then test those same rules through the generated form and API.
Frequently asked questions
Should we make every numeric-looking field a number input?
No. Use a number input for an actual measurable or incrementable quantity. Use a text-based field with a deliberate parsing policy for identifiers, formatted currency, locale-specific entry, or values where spinner behavior is not useful.
Does step prevent off-step values from being typed?
No. A user can manually enter a value that does not fit the configured step. Treat the invalid state as part of your validation flow and recheck it on the server.
Should we validate on every keystroke?
Usually not for required and range errors. Validate gently during editing only when the feedback is useful, then validate complete values on blur and all relevant fields on submit.
What should an empty numeric input submit?
That is a domain decision. A required quantity should produce a required-field error. An optional quantity can serialize as null or be omitted, as long as the client and server use the same contract.
Build a numeric input that stays predictable
A robust numericinput begins with a contract: which values are valid, what blank means, when input becomes committed, and how errors return from the server. From there, mapping min, max, step, labels, descriptions, and state into a Vue component becomes repeatable instead of fragile.
Start with one high-value quantity field, test its boundary values and error recovery, then reuse the same schema-to-control mapping across your generated layouts. When you are ready to configure the component itself, explore the DOM Studio Vue Number Input and adapt the example to your application’s numeric rules.
