Numeric input.
A field for a number you calculate with. Quantities on an order row, hours on a timesheet, a unit price, a percentage. It parses while you type and formats when you leave, and it is the only input in Sprout that turns tabular figures on.
What this is not
A number you calculate with is not the same as a number you identify with. Order numbers, phone numbers and postcodes are strings that happen to be digits, and they take a plain text field.
The test: would you ever add two of them together? If not, it is text.
Anatomy
Five parts. The label and the field are always there, the unit and the stepper buttons are optional. The label and the helper sit outside the field, so the field height stays exactly one token.
Hover or focus a row to see that measurement on the component above.
4px LabelAlways visible. Never a placeholder standing in for a label. The required mark is on surface, not danger: required is not an error, and the asterisk should not turn colour into the only carrier of the meaning.48px FieldHolds the value, an optional unit and optional stepper buttons. The value is 16px at every size, never smaller, because iOS zooms the page when you focus an input below 16px.12px UnitInside the field, after the number, never a control of its own. h, kr, %, pcs. Nordic convention puts kr after the value, not before it.48px Stepper buttonsOptional, and rarely earned. Each button is as wide as the field is tall, so the control grows by twice its own height before the value gets any room.4px HelperOne slot, two jobs: the format hint, then the error message in the same place, so nothing below the field moves when validation runs.Size
Three heights. Only the largest is usable with a gloved hand, which makes it the only one that belongs in the field apps.
With stepper buttons the control is height × 3 wide before the value gets any room. At Large that is 144px of chrome.
Kinds of number
The kind of number decides the keypad, the decimals and the unit. It is not a style variant, it is what the field is for, and getting it wrong shows up as the wrong keyboard on a phone.
Figures, separators and locale
This is where numeric fields actually break. None of it is visible until a user types a decimal or a total reaches four figures.
Manrope's default figures are proportional: measured at 32px, the digit 1 is 37% narrower than a 6. Sprout does not need a second typeface for numbers, it needs one line of CSS.
The shaded box behind each figure is the advance the font gives it. On the left the boxes stack; on the right the 1s are narrower, so the comma lands somewhere different on each row. Two figure sets in the same file, and the property picks one.
/* every numeric value, in a field or a table cell */
.sprout-numeric,
.sprout-input--numeric input,
td.numeric {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1; /* older Safari */
}
/* parse what a Nordic user actually types */
function parseNordic(raw) {
return parseFloat(
raw.replace(/[\s\u00A0]/g, '') // grouping spaces, incl. non-breaking
.replace(',', '.') // decimal comma
);
}States
Four blocks, light and dark side by side, each listing only the tokens it changes.
The same state wearing different content. Nothing but the value slot changes, so they share one set of tokens.
Light layers/outline is 1.61:1 against surface, under the 3:1 that WCAG 1.4.11 asks of a control boundary. Dark passes at 3.67:1. Moving light to the same neutral/550 would give 4.74:1 and one step for both modes.
The placeholder takes layers/supportive text, not a loose grey: a placeholder is text and owes the full 4.5:1.
Only the boundary changes. The ring is drawn outside the border, so the field does not resize and the number does not shift under the caret.
The border travels between modes, the ring does not. primary container is #152F1A in dark, 1.20:1 against surface, so the ring is invisible. It should resolve to an alpha of primary in dark.
Fires on blur, never on keystroke: typing 0,5 passes through 0, and 0 is not an error yet. The message replaces the format hint in place, so nothing below the field moves. Same hexagon wherever an error appears, so the shape carries the meaning too.
The icon carries the signal, so the message can take the token that reads best: on danger container, 14.98:1 on surface, the same hue and saturation as danger but darker. Dark inverts the pair, because there on danger container is nearly white, so the message takes danger at 8.94:1.
Light value on top, dark below. hovered and pressed belong to destructive controls; a field is not destructive. danger container is the block behind a validation summary, never behind a single field.
The border and the label do not move, so the field keeps its shape. Say why in the helper: a locked number with no reason reads as a bug. Read only keeps the surface fill and dashes the same outline border.
The one place a low ratio is correct: WCAG 1.4.3 exempts an inactive control, so the low contrast is the message. Never use this pair for a field that is merely empty.
Do & Don't
Four decisions that come up every time someone builds one.
AceveNumericInput) does not have these yet — it right-aligns, formats on blur only, and has no stepper prop. Every demo above is a page-local mock built to this spec, not the real component. Treat this as the target to build toward, not as what ships today.