Inputs
Number, currency and percentage
One numeric control, three unit contracts. The value the component reports is always a plain number in the canonical unit — formatting is presentation, and it never rewrites what you stored.
Examples
Number
Locale-aware parsing and formatting, caret-safe editing, arrow-key steps and real spinbutton semantics come from React Aria. LUC owns the shell and the steppers.
Integer, stepped by one.
Two decimals, no step grid — 1234.567 stays 1234.567.
Milliseconds; the unit is a suffix, not part of the value.
<FieldGroup columns={3}>
<Field label="Quantity" description="Integer, stepped by one.">
<NumberInput defaultValue={250} step={1} decimals={0} />
</Field>
<Field label="Price" description="Two decimals, no step grid — 1234.567 stays 1234.567.">
<NumberInput defaultValue={64321.15} decimals={2} />
</Field>
<Field label="Latency budget" description="Milliseconds; the unit is a suffix, not part of the value.">
<NumberInput defaultValue={250} min={0} max={5000} step={10} decimals={0} unit="ms" />
</Field>
</FieldGroup>Currency
The currency is a format, not a policy: the value is the major unit as a number (250000 renders $250,000.00), and any ISO-4217 code works.
USD
EUR, de-DE grouping and separators.
JPY has no minor unit; precision is configuration.
canonical value: 250000 · displayed as $250,000.00
<CurrencyInput currency="USD" value={notional} onValueChange={setNotional} />
<CurrencyInput currency="EUR" locale="de-DE" defaultValue={1250.5} />
<CurrencyInput currency="JPY" decimals={0} defaultValue={980000} />Percentage
`scale` is required, because 0.12 is ambiguous. The component converts only between the canonical value and the display value — it never guesses which one you meant.
scale="fraction" — the stored value is 0.045, displayed as 4.5%.
scale="percent" — the stored value is 8, displayed as 8.0%.
canonical value: 0.045 (a fraction, not percent points)
<PercentageInput scale="fraction" value={weight} onValueChange={setWeight} />
<PercentageInput scale="percent" defaultValue={8} />States
The same six states as every other LUC control: hover, focus, invalid, disabled and read-only share one shell.
Default
Invalid
commitBehavior="validate" reports the range instead of snapping to it
Disabled
Read-only
No steppers
compact filter bars
Small
<NumberInput invalid defaultValue={-0.5} aria-label="Invalid number" />
<NumberInput disabled defaultValue={200} aria-label="Disabled number" />
<NumberInput readOnly defaultValue={200} aria-label="Read-only number" />Usage
import { NumberInput, CurrencyInput, PercentageInput } from "@luc/ui/forms";<NumberInput value={lookback} onValueChange={setLookback} min={5} max={400} step={5} aria-label="Lookback (days)" />`value` plus `onValueChange` controls it; a number commits on blur or Enter, and `step` is what the steppers move by.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
CurrencyInput | 27 documented props | ({ currency, decimals, ...props }: CurrencyInputProps) => import("@types/react").JSX.Element |
NumberInput | 28 documented props | ({ value, defaultValue, onValueChange, onBlur, min, max, step, formatOptions, decimals, locale, size, unit, leadingIcon, steppers, invalid, disabled, readOnly, required, placeholder, name, id, isWhee… |
PercentageInput | 27 documented props | ({ scale, step, decimals, value, defaultValue, onValueChange, ...props }: PercentageInputProps) => import("@types/react").JSX.Element |
NumberInput props
Type: NumberInputProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
commitBehavior? | "snap" | "validate" | undefined | — | `snap` clamps to min/max/step on blur, which is React Aria's default. |
decimals? | number | undefined | — | Convenience for `minimumFractionDigits` + `maximumFractionDigits`. |
decrementAriaLabel? | string | undefined | — | |
defaultValue? | number | null | undefined | — | |
disabled? | boolean | undefined | — | |
formatOptions? | Intl.NumberFormatOptions | undefined | — | `Intl.NumberFormatOptions`. Use `CurrencyInput`/`PercentageInput` presets. |
id? | string | undefined | — | |
incrementAriaLabel? | string | undefined | — | |
inputClassName? | string | undefined | — | |
invalid? | boolean | undefined | — | |
isWheelDisabled? | boolean | undefined | — | Scroll-wheel editing is off by default; a page should not edit itself. |
leadingIcon? | ReactNode | — | |
locale? | string | undefined | — | BCP-47 locale. Inherits the surrounding locale when omitted. |
max? | number | undefined | — | |
min? | number | undefined | — | |
name? | string | undefined | — | |
onBlur? | (() => void) | undefined | — | Called on blur; convenient for engines that validate on touch. |
onValueChange? | ((value: number | null) => void) | undefined | — | |
placeholder? | string | undefined | — | |
readOnly? | boolean | undefined | — | |
required? | boolean | undefined | — | |
size? | ControlSize | undefined | — | |
step? | number | undefined | — | Step grid. Omit it and the field keeps the value you gave it — the steppers still move by React Aria's default (1, or 0.01 for a percentage). Set a step only when the domain reall… |
steppers? | boolean | undefined | — | Chevron steppers. Keyboard and screen readers work either way. |
unit? | ReactNode | — | Text or glyph shown after the value. Presentation only, never in the value. |
value? | number | null | undefined | — |
CurrencyInput props27 props
CurrencyInput props
Type: CurrencyInputProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
commitBehavior? | "snap" | "validate" | undefined | — | `snap` clamps to min/max/step on blur, which is React Aria's default. |
currency | string | — | ISO-4217 code. The component makes no assumption about which one. |
decimals? | number | undefined | — | Fraction digits. Defaults to 2 — a currency field that hides cents is a bug. |
decrementAriaLabel? | string | undefined | — | |
defaultValue? | number | null | undefined | — | |
disabled? | boolean | undefined | — | |
id? | string | undefined | — | |
incrementAriaLabel? | string | undefined | — | |
inputClassName? | string | undefined | — | |
invalid? | boolean | undefined | — | |
isWheelDisabled? | boolean | undefined | — | Scroll-wheel editing is off by default; a page should not edit itself. |
leadingIcon? | ReactNode | — | |
locale? | string | undefined | — | BCP-47 locale. Inherits the surrounding locale when omitted. |
max? | number | undefined | — | |
min? | number | undefined | — | |
name? | string | undefined | — | |
onBlur? | (() => void) | undefined | — | Called on blur; convenient for engines that validate on touch. |
onValueChange? | ((value: number | null) => void) | undefined | — | |
placeholder? | string | undefined | — | |
readOnly? | boolean | undefined | — | |
required? | boolean | undefined | — | |
size? | ControlSize | undefined | — | |
step? | number | undefined | — | Step grid. Omit it and the field keeps the value you gave it — the steppers still move by React Aria's default (1, or 0.01 for a percentage). Set a step only when the domain reall… |
steppers? | boolean | undefined | — | Chevron steppers. Keyboard and screen readers work either way. |
value? | number | null | undefined | — |
PercentageInput props27 props
PercentageInput props
Type: PercentageInputProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
aria-label? | string | undefined | — | |
className? | string | undefined | — | |
commitBehavior? | "snap" | "validate" | undefined | — | `snap` clamps to min/max/step on blur, which is React Aria's default. |
decimals? | number | undefined | — | Convenience for `minimumFractionDigits` + `maximumFractionDigits`. |
decrementAriaLabel? | string | undefined | — | |
defaultValue? | number | null | undefined | — | |
disabled? | boolean | undefined | — | |
id? | string | undefined | — | |
incrementAriaLabel? | string | undefined | — | |
inputClassName? | string | undefined | — | |
invalid? | boolean | undefined | — | |
isWheelDisabled? | boolean | undefined | — | Scroll-wheel editing is off by default; a page should not edit itself. |
leadingIcon? | ReactNode | — | |
locale? | string | undefined | — | BCP-47 locale. Inherits the surrounding locale when omitted. |
max? | number | undefined | — | |
min? | number | undefined | — | |
name? | string | undefined | — | |
onBlur? | (() => void) | undefined | — | Called on blur; convenient for engines that validate on touch. |
onValueChange? | ((value: number | null) => void) | undefined | — | |
placeholder? | string | undefined | — | |
readOnly? | boolean | undefined | — | |
required? | boolean | undefined | — | |
scale | "percent" | "fraction" | — | The unit contract, stated on purpose and never defaulted: - `fraction`: `0.12` is 12% (what `Intl` and most APIs use). - `percent`: `12` is 12% (what most people type and most for… |
size? | ControlSize | undefined | — | |
step? | number | undefined | — | |
steppers? | boolean | undefined | — | Chevron steppers. Keyboard and screen readers work either way. |
value? | number | null | undefined | — |
Guidelines
Do
- Use `NumberInput` for a measured quantity, `CurrencyInput` for money and `PercentageInput` for a ratio — the unit is part of the component, not a suffix someone typed.
- Give `step` the granularity the value really has: it snaps typed input onto the grid and it is what the steppers move by.
- Set `min` and `max` when the quantity has a floor or a ceiling the form has to enforce; the steppers stop at the ends.
Don't
- Use a text input with `parseFloat` in the handler: the component owns parsing, the decimal separator and the commit-on-blur rule.
- Leave `scale` off `PercentageInput`: a percentage without a declared scale is the ambiguous 0.12 the component exists to prevent.
- Format money with a currency symbol you typed — `currency` is the prop, and it drives the prefix and the grouping.
Accessibility
The control is a real `<input>` with `inputMode` set for the numeric keypad, and the steppers are buttons with names rather than decorative glyphs. A number commits on blur or Enter, so a partially typed value is never read as a different one, and `Field` supplies the label, the description and the message.
Related