A number typed exactly (numeric field, spin box): an amount, a weight, hours. It shows the unit inside the field, keeps the value in its range and steps with the arrow keys.
import { NumberInput } from "@umriss-ui/core";
The value is a number or null. An empty field is null, not zero: a figure nobody gave is not a figure of zero.
It writes and reads the number in the notation of the formats, so German users get a decimal comma through @umriss-ui/core/wording/de (ADR-0024). Every value reported is already clamped to the range; the text in the field follows when it is left.
The value is a number or null; the field writes it in the formats' notation and reads it back.
import { useState } from "react";
import { FormField, NumberInput } from "@umriss-ui/core";
export default function NumberField() {
const [weight, setWeight] = useState<number | null>(18.5);
return (
<FormField label="Parcel weight" style={{ maxWidth: 240 }}>
<NumberInput value={weight} onChange={setWeight} min={0} decimals={1} suffix="kg" />
</FormField>
);
}
Put the unit in prefix or suffix, inside the field, and fix the places with decimals.
import { useState } from "react";
import { FormField, Grid, NumberInput } from "@umriss-ui/core";
export default function UnitsAndDecimals() {
const [budget, setBudget] = useState<number | null>(68000);
const [discount, setDiscount] = useState<number | null>(2.5);
const [variance, setVariance] = useState<number | null>(-4.25);
return (
<Grid minItemWidth="200px" gap={4}>
<FormField label="Monthly budget" hint="Whole euros, step 500.">
<NumberInput value={budget} onChange={setBudget} min={0} decimals={0} step={500} suffix="€" />
</FormField>
<FormField label="Early payment discount" hint="One decimal place.">
<NumberInput value={discount} onChange={setDiscount} min={0} max={10} decimals={1} step={0.5} suffix="%" />
</FormField>
<FormField label="Forecast variance" hint="Negative allowed.">
<NumberInput value={variance} onChange={setVariance} decimals={2} step={0.25} prefix="±" suffix="%" />
</FormField>
</Grid>
);
}
An empty field reports null, not zero; errors come from the FormField, and disabled holds the figure fixed.
import { useState } from "react";
import { FormField, Grid, NumberInput, Text } from "@umriss-ui/core";
export default function States() {
const [actual, setActual] = useState<number | null>(null);
const [quantity, setQuantity] = useState<number | null>(0);
return (
<Grid minItemWidth="220px" gap={4}>
<FormField label="Actual, March" hint="Not booked yet.">
<NumberInput value={actual} onChange={setActual} decimals={0} suffix="€" />
<Text size="xs" tone="muted" mono>
value: {actual === null ? "null" : actual}
</Text>
</FormField>
<FormField label="Quantity" error="An invoice line needs at least one unit.">
<NumberInput value={quantity} onChange={setQuantity} min={0} decimals={0} />
</FormField>
<FormField label="VAT rate" hint="Set by the tax code.">
<NumberInput value={19} onChange={() => {}} disabled suffix="%" />
</FormField>
<FormField label="Annual budget, all cost centres" hint="Small size, a long figure.">
<NumberInput size="sm" value={9_048_000.5} onChange={() => {}} decimals={2} suffix="€" />
</FormField>
</Grid>
);
}
A FormField with an error marks the field invalid by itself. Without one, set invalid and tie your message to the field with aria-describedby.
import { useId, useState } from "react";
import { NumberInput, Stack, Text } from "@umriss-ui/core";
export default function WithAnError() {
const [quantity, setQuantity] = useState<number | null>(0);
const messageId = useId();
const error = quantity === null || quantity < 1 ? "An invoice line needs at least one unit." : undefined;
return (
<Stack gap={1} style={{ maxWidth: 280 }}>
<NumberInput
aria-label="Quantity"
value={quantity}
onChange={setQuantity}
min={0}
decimals={0}
invalid={error !== undefined}
aria-describedby={error ? messageId : undefined}
/>
{error && (
<Text id={messageId} size="xs" style={{ color: "var(--u-color-danger-text)" }}>
{error}
</Text>
)}
</Stack>
);
}
Set min and max: what onChange reports is always clamped, and the text follows when the field is left.
import { useState } from "react";
import { FormField, Grid, NumberInput } from "@umriss-ui/core";
export default function KeepAValueInRange() {
const [capacity, setCapacity] = useState<number | null>(32);
const [focus, setFocus] = useState<number | null>(60);
return (
<Grid minItemWidth="220px" gap={4}>
<FormField label="Capacity, Kofi Mensah" hint="0 to 48 hours a week; arrows count, Shift ×10.">
<NumberInput value={capacity} onChange={setCapacity} min={0} max={48} decimals={0} suffix="h" />
</FormField>
<FormField label="Share for project work" hint="0 to 100 %, step 5.">
<NumberInput value={focus} onChange={setFocus} min={0} max={100} step={5} decimals={0} suffix="%" />
</FormField>
</Grid>
);
}
Several number fields feed one derived figure; the total is computed from the values and never stored beside them.
import { useState } from "react";
import { Card, CardBody, CardHeader, FormField, Grid, NumberInput, Stack, Text, useFormats } from "@umriss-ui/core";
export default function PriceAnInvoiceLine() {
const formats = useFormats();
const [quantity, setQuantity] = useState<number | null>(40);
const [unitPrice, setUnitPrice] = useState<number | null>(12.9);
const [discount, setDiscount] = useState<number | null>(5);
const net = (quantity ?? 0) * (unitPrice ?? 0) * (1 - (discount ?? 0) / 100);
return (
<Card style={{ maxWidth: 560 }}>
<CardHeader eyebrow="INV-2031, line 2" title="Printer paper, A4, 500 sheets" />
<CardBody>
<Stack gap={4}>
<Grid minItemWidth="140px" gap={3}>
<FormField label="Quantity">
<NumberInput value={quantity} onChange={setQuantity} min={0} decimals={0} />
</FormField>
<FormField label="Unit price">
<NumberInput value={unitPrice} onChange={setUnitPrice} min={0} decimals={2} step={0.1} suffix="€" />
</FormField>
<FormField label="Discount">
<NumberInput value={discount} onChange={setDiscount} min={0} max={100} decimals={1} suffix="%" />
</FormField>
</Grid>
<Text size="sm">
Line total, net: <Text as="span" mono>{formats.number(net, 2)} €</Text>
</Text>
</Stack>
</CardBody>
</Card>
);
}
| Key | Action |
|---|---|
Arrow Up / Arrow Down |
Steps the value up or down by one step. |
Shift + Arrow Up / Down |
Steps by ten steps. |
NumberInputProps| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | number | null | — | The number, or null for "not stated". An empty field is not zero:
a quantity that was not measured is not a quantity of zero.Shown in: Configurator, Number field, Units and decimals and 13 more |
min | number | — | Lower bound. Every reported value is clamped; the text in the field
follows on leaving. Shown in: Configurator, Number field, Units and decimals and 11 more |
max | number | — | Upper bound. Every reported value is clamped; the text in the field
follows on leaving. Shown in: Configurator, Units and decimals, Keep a value in range and 5 more |
step | number | 1 | Step size for the arrow keys; Shift multiplies it by ten. Shown in: Configurator, Units and decimals, Keep a value in range and 3 more |
decimals | number | — | Fixed decimal places; 0 = whole numbers, undefined = free. Shown in: Configurator, Number field, Units and decimals and 11 more |
prefix | ReactNode | — | Adornment before the number, e.g. "±". Shown in: Units and decimals |
suffix | ReactNode | — | Adornment after the number, e.g. "€" or "%". Shown in: Number field, Units and decimals, States and 9 more |
size | "sm" | "md" | the size of a ControlSizeProvider around it, else "md" | sm for a toolbar and dense forms, md otherwise.Shown in: Configurator, States, Set up a team (Scenarios) |
chars | number | — | The width in characters - room for the number; the field adds its own
padding and steppers, and the room for a prefix and suffix given as
text. Given, the field is that wide wherever it stands, and never wider
than its place. Without it the field fills its place, and is 10
characters wide where the place asks - in a toolbar or a row.Shown in: Configurator, Sized to the value (Sizes) |
invalid | boolean | — | Marks the field as invalid. FormField sets it itself as soon as it
carries an error – by hand only needed without FormField.Shown in: Configurator, With an error |
ref | Ref<HTMLInputElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
onChangerequired | (value: number | null) => void | — | Runs on every valid input, with null for the emptied field – and
always clamped: what goes out lies between min and max, by every
route, while typing included. Only the text in the field may still
stand outside while typing; on leaving it is set to the clamped
value.Shown in: Configurator, Number field, Units and decimals and 13 more |
Also takes every attribute of <input> – without onChange, prefix, size, type, value.
What umriss deliberately does not build, and why: ADR-0032.