Number Input
Precision-safe number field with stepper buttons — sizes, layouts, range, affixes, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Number Input is a number field with stepper buttons. It is precision-safe: values are
strings and all arithmetic runs through bignumber.js, so very large counts and high-decimal
measurements never lose precision the way a JavaScript number would. It works controlled or
uncontrolled and clamps to min/max. In React it's <NumberInput>; in Angular it's
<bpdm-number-input>.
Usage
Drop it in with a starting value. Type freely, or use the steppers.
import { NumberInput } from '@bpdm/ui/number-input';
export function Quantity() {
return <NumberInput defaultValue="9" min="0" />;
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'quantity',
imports: [BpdmNumberInput],
template: `<bpdm-number-input defaultValue="9" min="0" />`,
})
export class Quantity {}Range & step
min and max clamp the value (the relevant stepper disables at the bound); step sets how much
each press adds or removes.
import { NumberInput } from '@bpdm/ui/number-input';
export function Range() {
return (
<div className="flex items-center gap-4">
<NumberInput min="0" max="10" defaultValue="5" />
<NumberInput step="5" defaultValue="25" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'range',
imports: [BpdmNumberInput],
template: `
<div class="flex items-center gap-4">
<bpdm-number-input min="0" max="10" defaultValue="5" />
<bpdm-number-input step="5" defaultValue="25" />
</div>
`,
})
export class Range {}Precision
The value is a string and every step/clamp runs through bignumber.js, so high-decimal steps
and integers beyond Number.MAX_SAFE_INTEGER stay exact — no float drift, no exponential
notation. Read the value as a string via onValueChange (React) or [(value)] (Angular).
import { useState } from 'react';
import { NumberInput } from '@bpdm/ui/number-input';
export function Precision() {
const [value, setValue] = useState('1.2500');
return (
<div className="flex items-center gap-4">
<NumberInput value={value} onValueChange={setValue} step="0.0001" suffix="kg" />
<NumberInput defaultValue="9007199254740993" />
</div>
);
}import { Component, signal } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'precision',
imports: [BpdmNumberInput],
template: `
<div class="flex items-center gap-4">
<bpdm-number-input [(value)]="value" step="0.0001" suffix="kg" />
<bpdm-number-input defaultValue="9007199254740993" />
</div>
`,
})
export class Precision {
readonly value = signal('1.2500');
}Prefix & suffix
Add static text before (prefix) or after (suffix) the value — units like GB, kg, seats,
or a × multiplier.
import { NumberInput } from '@bpdm/ui/number-input';
export function Affix() {
return (
<div className="flex items-center gap-4">
<NumberInput defaultValue="50" suffix="GB" min="0" />
<NumberInput defaultValue="5" suffix="seats" min="1" />
<NumberInput defaultValue="2" prefix="×" min="1" buttonLayout="horizontal" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'affix',
imports: [BpdmNumberInput],
template: `
<div class="flex items-center gap-4">
<bpdm-number-input defaultValue="50" suffix="GB" min="0" />
<bpdm-number-input defaultValue="5" suffix="seats" min="1" />
<bpdm-number-input defaultValue="2" prefix="×" min="1" buttonLayout="horizontal" />
</div>
`,
})
export class Affix {}Button layout
buttonLayout chooses the steppers — stacked (up/down chevrons on the right, default) or
horizontal (−/+ on each side).
import { NumberInput } from '@bpdm/ui/number-input';
export function Layout() {
return (
<div className="flex items-center gap-4">
<NumberInput buttonLayout="stacked" defaultValue="9" />
<NumberInput buttonLayout="horizontal" defaultValue="9" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'layout',
imports: [BpdmNumberInput],
template: `
<div class="flex items-center gap-4">
<bpdm-number-input buttonLayout="stacked" defaultValue="9" />
<bpdm-number-input buttonLayout="horizontal" defaultValue="9" />
</div>
`,
})
export class Layout {}Sizes
size sets the height and text scale — sm, md (default), or lg.
import { NumberInput } from '@bpdm/ui/number-input';
export function Sizes() {
return (
<div className="flex items-center gap-4">
<NumberInput size="sm" defaultValue="9" />
<NumberInput size="md" defaultValue="9" />
<NumberInput size="lg" defaultValue="9" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'sizes',
imports: [BpdmNumberInput],
template: `
<div class="flex items-center gap-4">
<bpdm-number-input size="sm" defaultValue="9" />
<bpdm-number-input size="md" defaultValue="9" />
<bpdm-number-input size="lg" defaultValue="9" />
</div>
`,
})
export class Sizes {}States
Disabled dims the field and blocks both typing and the steppers.
import { NumberInput } from '@bpdm/ui/number-input';
export function States() {
return <NumberInput disabled defaultValue="9" />;
}import { Component } from '@angular/core';
import { BpdmNumberInput } from '@bpdm/ng';
@Component({
selector: 'states',
imports: [BpdmNumberInput],
template: `<bpdm-number-input disabled defaultValue="9" />`,
})
export class States {}API
NumberInput / bpdm-number-input
Values accept a string or number; strings are recommended to preserve precision.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | number | — | Controlled value. React: pair with onValueChange. Angular: two-way [(value)]. |
defaultValue | string | number | "0" | Uncontrolled initial value. |
min | string | number | — | Lower bound; the decrease stepper disables at it. |
max | string | number | — | Upper bound; the increase stepper disables at it. |
step | string | number | 1 | Amount each stepper press adds or removes. |
size | sm | md | lg | md | Height + text scale. |
buttonLayout | stacked | horizontal | stacked | Stepper placement. |
prefix | string | — | Static text before the value. |
suffix | string | — | Static text after the value. |
onValueChange (React) | (value: string) => void | — | Called with the string value on every change. |
messages | { increase, decrease } | English | Override the stepper button labels (screen-reader text) — see Internationalization. |
disabled | boolean | false | Disable the field and steppers. |
className (React) / class (Angular) | string | — | Extra classes on the wrapper. |
Accessibility
- The wrapper is a
role="group", and each stepper is a real<button>with anaria-label(Increase/Decrease) — usable and announced without sight of the glyph. - The field uses
inputmode="decimal", so mobile shows a numeric keypad. - At
min/maxthe relevant stepper isdisabled(removed from the tab order), so the value can't leave its range. - Give the field a name — associate a
<label>(viahtmlFor/id) or addaria-label.
Internationalization
- The stepper button labels (
Increase/Decrease) are translatable via themessagesprop — see Translating labels. Give the field itself a translated accessible name. - RTL-safe: field alignment uses a logical property (
text-start) and the steppers sit via flex layout, so the control mirrors underdir="rtl".