NumberInput
Segmented numeric input — [ − ][ value ][ + ] — with locale-aware parsing via React Aria's NumberField.
Built on React Aria NumberField — keyboard support for ↑/↓, PageUp/PageDown, Home/End and wheel; min/max are announced.
A segmented numeric control — a tinted track wrapping three raised segments,
[ − ][ value ][ + ]. The centre segment is a real, editable field (type into
it, or use the keyboard), and the large − / + buttons give touch-friendly
targets (~44px tall at md). Parsing, formatting, clamping, and keyboard
behaviour come from React Aria’s NumberField.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ width: '200px' }}>
<NumberInput aria-label="Quantity" defaultValue={1} minValue={0} maxValue={10} />
</div>
);
}
Variants
Section titled “Variants”variant tints the track without changing the segmented structure: outline
(default — light well + hairline border), filled (deeper solid tint, no
border), and ghost (no well; segments stay flat until hover).
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '220px' }}>
<NumberInput aria-label="Outline" variant="outline" defaultValue={1} />
<NumberInput aria-label="Filled" variant="filled" defaultValue={1} />
<NumberInput aria-label="Ghost" variant="ghost" defaultValue={1} />
</div>
);
}
Three sizes are available: sm, md (default), and lg.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '200px' }}>
<NumberInput aria-label="Small" size="sm" defaultValue={3} />
<NumberInput aria-label="Medium" size="md" defaultValue={3} />
<NumberInput aria-label="Large" size="lg" defaultValue={3} />
</div>
);
}
Range and step
Section titled “Range and step”Use minValue, maxValue, and step to constrain the input. Stepper buttons disable at bounds.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '200px' }}>
<NumberInput aria-label="0–100" defaultValue={25} minValue={0} maxValue={100} step={5} />
<NumberInput aria-label="Decimal" defaultValue={1.5} step={0.1} minValue={0} maxValue={5} />
</div>
);
}
Prefix and suffix
Section titled “Prefix and suffix”Use prefix and suffix to inline currency, units, or other muted affixes.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '240px' }}>
<NumberInput aria-label="Price" defaultValue={9.99} prefix="$" minValue={0} step={0.01} />
<NumberInput aria-label="Weight" defaultValue={70} suffix="kg" minValue={0} />
<NumberInput aria-label="Discount" defaultValue={20} suffix="%" minValue={0} maxValue={100} />
</div>
);
}
Formatting
Section titled “Formatting”Pass formatOptions (forwarded to React Aria) to render currency, percent, or a fixed precision.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '240px' }}>
<NumberInput
aria-label="Currency"
defaultValue={1299}
formatOptions={{ style: 'currency', currency: 'USD' }}
/>
<NumberInput
aria-label="Percent"
defaultValue={0.42}
minValue={0}
maxValue={1}
step={0.01}
formatOptions={{ style: 'percent' }}
/>
</div>
);
}
States
Section titled “States”import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', width: '200px' }}>
<NumberInput aria-label="Default" defaultValue={5} />
<NumberInput aria-label="Invalid" defaultValue={-1} invalid />
<NumberInput aria-label="Disabled" defaultValue={5} isDisabled />
<NumberInput aria-label="Read-only" defaultValue={5} isReadOnly />
</div>
);
}
Touch interactions
Section titled “Touch interactions”- Hold to repeat. Press and hold
−/+to step continuously — it waits a short delay, then repeats on an accelerating interval, and stops at the min/max bound. This is React Aria’s built-in press-and-hold, so it works for mouse, touch, and pen. - Pressed feedback. The
−/+segments visibly depress into the accent colour on press. - Long-press to clear. Opt in with
clearOnLongPress— a ~500ms long-press on the value segment clears it tominValue(or empty when no minimum is set). Off by default.
import { NumberInput } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<div style={{ width: '220px' }}>
<NumberInput
aria-label="Budget"
defaultValue={1280}
minValue={0}
prefix="$"
clearOnLongPress
/>
</div>
);
}
Accessibility
Section titled “Accessibility”- Built on React Aria
NumberField— values are parsed locale-correctly and clamped tominValue/maxValue. - Keyboard:
ArrowUp/ArrowDownstep,PageUp/PageDownstep by a larger amount,Home/Endjump to bounds. Hold-to-repeat is pointer-only; keyboard repeat is the OS key-repeat as before. - The editable value is a real
role="spinbutton"input witharia-valuenow/min/max. The−/+segments are real buttons with localized labels (incrementLabel/decrementLabel); affixes are decorative (aria-hidden). aria-invalidis mirrored frominvalid(orisInvalid) on the underlying input.- The focus ring is drawn on the track and honours
:focus-visible; the acceleration / press transitions respectprefers-reduced-motion.
Recipes
Section titled “Recipes”- Use
formatOptions={{ style: "currency", currency: "USD" }}for money fields — React Aria handles parsing and display. - Pass
steptogether withminValue/maxValueto bound the field and ensure the steppers disable at the bounds. - Provide
incrementLabel/decrementLabelto localize the stepper announcement. - Enable
clearOnLongPressfor touch-first quantity fields where a quick reset to the minimum is handy.