GrNumberInput

  • core
  • forms
  • API · 34 props
  • ⌨ keys

A number field with stepper controls and range constraints.

import { GrNumberInput } from '@feugene/granularity/components/GrNumberInput'

Overview

When to take it

  • the value is numeric — a quantity, a price, a percentage: v-model gives a number rather than a string;
  • there is a step and there are bounds — step, min, max with ± buttons and hold-to-repeat;
  • precision matters — precision together with a separator and digit grouping by locale;
  • an empty value is allowed — null means “not filled in” rather than zero.

When to take something else

NeedTake
The value is set by draggingGrSlider
The value is textGrInput
The number is a rating in starsGrRating
A choice from a fixed set of numbersGrSelect
Show a number rather than enter oneGrStatistic

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

Variants · GrNumberInput
state \ shapeboxpill
default
success
warning
danger

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • readonly
  • invalid

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

Stage

Loading…

0 changed· state is in the URL

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". max is 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.00
    Canvas tote€18.00 · 12 in stock
    €36.00
    Wool cardigan, navy, L€129.00 · last one
    €129.00
    Subtotal€229.00
  • A quote in German format

    locale="de-DE" with decimal-separator="," shows 1.284,50 and accepts «1284,5», while the model keeps 1284.5. use-grouping adds thousands only when the field is not being edited, and precision rounds 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 max is 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

PropTypedefaultDescription
disabledboolean | undefinedfalseDisable the field: input, the ± buttons and clearing are unavailable. It adds up with `GrFormField`.
readonlyboolean | undefinedfalseRead-only: the value is visible and leaves with the form, but is not edited.
invalidboolean | undefinedfalseA quick invalidity flag; the equivalent of `state='danger'` plus `aria-invalid`.
requiredboolean | undefinedfalseA required field (`aria-required`). It adds to the `required` of `GrFormField`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height and type size on the control scale. `md` by default; taken from `GrConfigProvider`.
placeholderstring | undefinedundefinedHint in an empty field. It does not replace the label: the name is given by `GrFormField` or `ariaLabel`.
ariaLabelstring | undefinedundefinedThe accessible name outside `GrFormField`.
clearableboolean | undefinedundefinedA button that clears the value.
clearLabelstring | undefinedundefinedThe a11y label of the clear button.
namestring | undefinedundefinedThe native `name` — the name of the field on form submission.
prefixMinWidthstring | undefinedundefinedThe minimum width of the prefix. By default — by the size step.
prefixMaxWidthstring | undefinedundefinedThe maximum width of the prefix: wider content is clipped.
suffixMinWidthstring | undefinedundefinedThe minimum width of the suffix. By default — by the size step.
suffixMaxWidthstring | undefinedundefinedThe maximum width of the suffix: wider content is clipped.

Slots

SlotTypeDescription
prefixanyAn addon to the left of the field: a currency sign, an icon.
suffixanyAn addon to the right of the field: a unit of measurement.

Events

EventSignatureDescription
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

MethodsSignatureDescription
focus() => voidPut the focus in the field.
blur() => voidRemove 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
import type { GrNumberInputConfigurableProps, GrNumberInputProps, GrNumberInputEmits } from '@feugene/granularity/components/GrNumberInput'

Filedist/types/components/GrNumberInput/config.d.ts

grNumberInputConfigconst
const grNumberInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrNumberInput">;

Filedist/types/components/GrNumberInput/defaults.d.ts

GrNumberInputConfigurablePropsinterface
interface GrNumberInputConfigurableProps {
    size: GrNumberInputSize;
    clearable: boolean;
    shape: GrControlShape;
}

Filedist/types/components/GrNumberInput/safelist.d.ts

grNumberInputSafelistconst
const grNumberInputSafelist: string[];

Filedist/types/components/GrNumberInput/GrNumberInput.vue.d.ts

GrNumberInputPropsinterface
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;
}
GrNumberInputEmitsinterface
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

GrNumberInputStatetype
type GrNumberInputState = GrControlState;
GrNumberInputSizetype
type GrNumberInputSize = GrComponentSize;
GrNumberInputControlsDirectiontype
type GrNumberInputControlsDirection = 'vertical' | 'horizontal';
GrNumberInputTextAligntype
type GrNumberInputTextAlign = 'left' | 'center' | 'right';
NumberInputSizetype
type NumberInputSize = GrNumberInputSize;
NumberInputControlsDirectiontype
type NumberInputControlsDirection = GrNumberInputControlsDirection;

Filedist/types/components/GrNumberInput/index.d.ts

GrNumberInputInstancetype
type GrNumberInputInstance = ComponentExposed<typeof GrNumberInputComponent>;

Accessibility

APG pattern

spinbutton

Keys

↑ / ↓PageUp / PageDownHome / End

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 null rather 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
vue
<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.