GrNumberInput
A number field with stepper controls and range constraints.
Overview
When to take it
- the value is numeric — a quantity, a price, a percentage:
v-modelgives anumberrather than a string; - there is a step and there are bounds —
step,min,maxwith ± buttons and hold-to-repeat; - precision matters —
precisiontogether with a separator and digit grouping by locale; - an empty value is allowed —
nullmeans “not filled in” rather than zero.
When to take something else
| Need | Take |
|---|---|
| The value is set by dragging | GrSlider |
| The value is text | GrInput |
| The number is a rating in stars | GrRating |
| A choice from a fixed set of numbers | GrSelect |
| Show a number rather than enter one | GrStatistic |
The difference from GrInput type="number" is not cosmetic: a native numeric field gives away a
string, loses the value on invalid input and behaves differently across browsers. Here the model
is number | null, and null differs from zero.
Variants every tone and view, rendered by the real component
state \ shape | box | pill |
|---|---|---|
default | ||
success | ||
warning | ||
danger |
Sizes
xssmmdlg
States
normaldisabledreadonlyinvalid
The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrNumberInput
v-model="value"
controls
:min="1"
:max="50"
/>Examples 3 each with a live demo and its code
Quantities in a bag
On a touch screen the buttons sit on both sides —
controls-direction="horizontal".maxis the stock, so «+» dims at it instead of silently doing nothing, and a number typed by hand is clamped on commit; line totals and the subtotal follow the model.Your bag Linen shirt, sand, M€64.00 · 4 in stock€64.00Canvas tote€18.00 · 12 in stock€36.00Wool cardigan, navy, L€129.00 · last one€129.00Subtotal€229.00A quote in German format
locale="de-DE"withdecimal-separator=","shows 1.284,50 and accepts «1284,5», while the model keeps 1284.5.use-groupingadds thousands only when the field is not being edited, andprecisionrounds on commit, not under the cursor.Angebot AN-2026-118 · Brauhaus Weber GmbHShown and typed in German format; the model holds plain numbers Gesamt netto4.658,09 €Autoscaling bounds that lean on each other
The minimum’s
maxis the current maximum and the other way round, so the buttons dim exactly where the pair would cross. The CPU target steps by 5, and PageUp/PageDown move by ten steps or a tenth of the range.Autoscaling · api-productioneu-west-1 · 4 instances running Between 2 and 8 instances; a new one starts when CPU stays above 65% for 3 minutes.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Disable the field: input, the ± buttons and clearing are unavailable. It adds up with `GrFormField`. |
readonly | boolean | undefined | false | Read-only: the value is visible and leaves with the form, but is not edited. |
invalid | boolean | undefined | false | A quick invalidity flag; the equivalent of `state='danger'` plus `aria-invalid`. |
required | boolean | undefined | false | A required field (`aria-required`). It adds to the `required` of `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The height and type size on the control scale. `md` by default; taken from `GrConfigProvider`. |
placeholder | string | undefined | undefined | Hint in an empty field. It does not replace the label: the name is given by `GrFormField` or `ariaLabel`. |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`. |
clearable | boolean | undefined | undefined | A button that clears the value. |
clearLabel | string | undefined | undefined | The a11y label of the clear button. |
name | string | undefined | undefined | The native `name` — the name of the field on form submission. |
prefixMinWidth | string | undefined | undefined | The minimum width of the prefix. By default — by the size step. |
prefixMaxWidth | string | undefined | undefined | The maximum width of the prefix: wider content is clipped. |
suffixMinWidth | string | undefined | undefined | The minimum width of the suffix. By default — by the size step. |
suffixMaxWidth | string | undefined | undefined | The maximum width of the suffix: wider content is clipped. |
prefixFixed | boolean | undefined | false | A fixed width for the prefix/suffix: a rigid width (from `*MaxWidth` → `*MinWidth` → the default) plus clipping of the content at the edge (the prefix on the right, the suffix on the left). By default the addons stretch to the content. |
suffixFixed | boolean | undefined | false | A fixed width for the suffix — as with `prefixFixed`, only the excess is clipped on the left. |
shape | "box" | "pill" | undefined | undefined | The shape of the border. `box` — the rounding of the controls scale; `pill` — a pill. The padding of the field and the overhang of the steppers follow the shape: `overflow-hidden` clips them along the outer arc, but they must not stand right up against it. |
max | number | undefined | undefined | The upper bound: applied when the input is committed, not on every keystroke. `End` sets it. |
id | string | undefined | undefined | The `id` of the field. Unset — taken from `GrFormField`, so that the label finds the field. |
locale | string | undefined | undefined | The BCP-47 locale for displaying the value (digit grouping and separators through `Intl.NumberFormat`). It works together with `useGrouping`. |
precision | number | undefined | undefined | How many digits after the separator to keep on commit. Unset — no rounding. |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | Highlighting of the border by the developer's decision. `success` and `warning` add an icon and a hidden label; `invalid` is stronger. `default` by default. |
autocomplete | string | undefined | undefined | The native `autocomplete`: what the browser should fill the field with. |
inputmode |
| "decimal" | The native `inputmode`: which on-screen keyboard to show. `decimal` by default. |
textAlign | GrNumberInputTextAlign | undefined | "left" | The alignment of the number in the field. `left` by default. |
decimalSeparator | string | undefined | "." | The decimal separator on screen. On input both `.` and `,` are accepted, and the model always holds a number. `.` by default. |
step | number | undefined | 1 | The step of the ± buttons and the `↑`/`↓` arrows; `PageUp`/`PageDown` take larger steps. `1` by default. |
min | number | undefined | undefined | The lower bound: applied when the input is committed, not on every keystroke. `Home` sets it. |
useGrouping | boolean | undefined | false | Group the digits when displaying (while the field is not focused). On focus the "raw" value is shown for editing. Off by default. |
controls | boolean | undefined | false | Whether to show the +/- buttons. |
controlsDirection | GrNumberInputControlsDirection | undefined | "vertical" | The layout of the ± buttons: `vertical` — stacked on the right, `horizontal` — on both sides of the field. `vertical` by default. |
increaseLabel | string | undefined | undefined | An i18n-friendly aria-label for the "increase" button. |
decreaseLabel | string | undefined | undefined | An i18n-friendly aria-label for the "decrease" button. |
modelValuerequiredv-model | number | null | — | The value of the field. `null` means empty. An unfinished entry ("-", "1,") is not a number and does not reach the model: while it is being typed the field keeps it in an internal draft, and the model honestly says "there is no number yet". |
Slots
| Slot | Type | Description |
|---|---|---|
prefix | any | An addon to the left of the field: a currency sign, an icon. |
suffix | any | An addon to the right of the field: a unit of measurement. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: number | null): void | — |
change | (event: "change", value: number | null): void | — |
clear | (event: "clear"): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
focus | () => void | Put the focus in the field. |
blur | () => void | Remove the focus from the field. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrNumberInputConfigurableProps, GrNumberInputProps, GrNumberInputEmits } from '@feugene/granularity/components/GrNumberInput'Filedist/types/components/GrNumberInput/config.d.ts
const grNumberInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrNumberInput">;Filedist/types/components/GrNumberInput/defaults.d.ts
interface GrNumberInputConfigurableProps {
size: GrNumberInputSize;
clearable: boolean;
shape: GrControlShape;
}Filedist/types/components/GrNumberInput/safelist.d.ts
const grNumberInputSafelist: string[];Filedist/types/components/GrNumberInput/GrNumberInput.vue.d.ts
interface GrNumberInputProps {
modelValue: number | null;
placeholder?: string;
autocomplete?: string;
inputmode?: InputHTMLAttributes['inputmode'];
disabled?: boolean;
invalid?: boolean;
readonly?: boolean;
required?: boolean;
ariaLabel?: string;
state?: GrNumberInputState;
name?: string;
id?: string;
size?: GrNumberInputSize;
shape?: GrControlShape;
textAlign?: GrNumberInputTextAlign;
decimalSeparator?: string;
step?: number;
min?: number;
max?: number;
precision?: number;
clearable?: boolean;
clearLabel?: string;
locale?: string;
useGrouping?: boolean;
controls?: boolean;
controlsDirection?: GrNumberInputControlsDirection;
prefixMinWidth?: string;
prefixMaxWidth?: string;
suffixMinWidth?: string;
suffixMaxWidth?: string;
prefixFixed?: boolean;
suffixFixed?: boolean;
increaseLabel?: string;
decreaseLabel?: string;
}interface GrNumberInputEmits {
(e: 'update:modelValue', value: number | null): void;
(e: 'change', value: number | null): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
(e: 'clear'): void;
}Filedist/types/components/GrNumberInput/grNumberInputStyles.d.ts
type GrNumberInputState = GrControlState;type GrNumberInputSize = GrComponentSize;type GrNumberInputControlsDirection = 'vertical' | 'horizontal';type GrNumberInputTextAlign = 'left' | 'center' | 'right';type NumberInputSize = GrNumberInputSize;type NumberInputControlsDirection = GrNumberInputControlsDirection;Filedist/types/components/GrNumberInput/index.d.ts
type GrNumberInputInstance = ComponentExposed<typeof GrNumberInputComponent>;Accessibility
APG pattern
spinbutton
Keys
↑ / ↓PageUp / PageDownHome / End
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The value is a number, the input is a draft
v-model is number | null, where null means “empty”. The same as in
GrSlider: a numeric control gives away a number rather than a string the
consumer has to parse themselves.
The objection to a numeric model went like this: an unfinished input (”-”, “1,”) is not a number,
and turning it into NaN on every keystroke is not allowed. It is answered by a division of
roles — an unfinished entry simply is not obliged to be the model:
- the draft is a string exactly as it is being typed. It lives inside the field until the entry is finished, and it is exactly what is shown on the screen;
- the model is a number. While the draft does not parse into a number, the model says
nullrather than storing a half-finished thing.
A commit (change, the loss of focus, a step by a button or a key) removes the draft: min/max
and precision are applied, and the display is computed from the model again. That is exactly why
precision="2" does not turn “7” into “7.00” right under the cursor — only when the entry is
finished.
decimalSeparator stops being part of the value and remains what it was — a way of displaying
and entering the fractional part. In the model 1,25 with decimal-separator="," lies as
1.25.
The ± buttons
<GrNumberInput v-model="qty" controls :min="1" :max="10" />controls shows the buttons, and controlsDirection puts them in a column on the right or on
both sides of the field.
A button dims at its own bound — at the maximum the ”+” is unavailable rather than silently
idle. In readonly both dim: a button that is known to be refused is misleading. Holding a button
steps repeatedly (a pause of 400 ms, then every 60 ms) and stops at a bound, on release and when
the cursor leaves. The final click of such a hold gives no step — that is the tail of a gesture
rather than a new intent. A keyboard activation (Enter/Space) never falls under that rule: it
always steps, even if the gesture before it was interrupted without a click.
The button does not take the focus for itself: having pressed Enter on the ”+”, a keyboard user
stays on it and can press again. The field is focused only when the step came from it — from the
↑/↓ arrows.
The step
step sets the size of a step, and precision how many digits to keep. A fractional step does
not accumulate binary error even without precision: the result is rounded to the number of
digits of the larger operand, so three presses of ↑ with step="0.1" give 0.3 rather than
0.30000000000000004.
PageUp/PageDown step coarsely — by the same rule as in GrSlider: ten steps or a tenth of
the range, whichever is larger. Without min/max set there is no range, and ten steps remain.
Grouping and the locale
useGrouping shows the value with digit separators while the field is not focused: while
editing, the grouping would get in the way. The locale comes from the locale prop, and if it is
not set, from the i18n adapter of the package, so in a multilingual application it does not have
to be passed to every field.
With grouping, the formatted value goes into aria-valuetext — otherwise a screen reader would
read the raw number. Without grouping the attribute is not set: aria-valuenow already carries
the same thing.
Clearing and the events
clearable adds a cross; it is not shown when the value is empty, in readonly and in
disabled. Clearing emits clear and returns the focus to the field.
The set of events matches GrInput: update:modelValue, change, focus,
blur, clear.
The states
state (default | success | warning | danger) sets the hue of the border, and invalid forces
the red one.
success and warning additionally show an icon and a hidden caption that goes into the
field’s aria-describedby: the colour of the border as the only carrier of meaning is
WCAG 1.4.1, and these two states have no ARIA equivalent of aria-invalid.
danger gets no icon — its meaning is already carried by aria-invalid and the error text
from GrFormField — and the sign goes out as soon as the field is declared invalid.
disabled dims the field with the --gr-disabled-* tokens rather than with
transparency: opacity dilutes text colours tuned to AA.
Readonly
A readonly field behaves like text: the arrows, PageUp/PageDown and Home/End are given
to the native caret, and the value is changed neither by keys, nor by the ”±” buttons (they are
unavailable in this state), nor by the auto-repeat.
Props in this example