NumberInput

Lets users enter a numeric value by typing or stepping.

Installation

  1. Install the package.
npm install @hashintel/ds-components
  1. Import the component.
import { NumberInput } from "@hashintel/ds-components";

export const RetryLimitInput = ({
  value,
  onChange,
}: {
  value: number | null;
  onChange: (value: number | null) => void;
}) => (
  <NumberInput
    value={value}
    onChange={onChange}
    min={0}
    max={10}
    width="xs"
    aria-label="Retry limit"
  />
);

When to use

  • Do: Use NumberInput when the stored value is a number: counts, limits, timeouts, numeric entity properties.
  • Do: Set min, max, step and inputMode to match the value's constraints — stepping stays in range and mobile devices get the matching keyboard.
  • Do: Wrap it in Form.Field for a label, description, and error messages — the label connects to the input automatically.

When not to use

  • Don't: Don't use it for numeric codes — IDs, phone numbers, postal codes — where leading zeros matter and stepping makes no sense. Use TextInput with inputMode="numeric".
  • Don't: Don't use it when the rough position in a bounded range matters more than the exact figure — use Slider.
  • Don't: Don't expect formatted text while typing (thousands separators, currency symbols) — the field holds a plain number. Use styledValue for a formatted unfocused display and a prefix or suffix for symbols.

Examples

Basic

NumberInput is controlled: pass value and take the parsed number from onChange, which reports null when the field is empty.

import { useState } from "react";

import { NumberInput } from "@hashintel/ds-components";

export const BatchSizeInput = () => {
  const [batchSize, setBatchSize] = useState<number | null>(32);

  return (
    <NumberInput
      value={batchSize}
      onChange={(value) => setBatchSize(value)}
      min={1}
      max={512}
      width="xs"
      aria-label="Batch size"
    />
  );
};

Decimal values

step defaults to 1, which makes an integer input: decimal-separator keys are rejected and parsed values are truncated. A fractional step — or "any" — switches to decimal mode and the decimal mobile keyboard.

Minimum and maximum

min defaults to 0 — pass a negative min to allow negative values. The bounds constrain stepping only: users can still type outside the range, so validate the value and set invalid yourself.

States

Disabled, invalid, and loading, in that order. invalid is presentational — an out-of-range typed value doesn't set it automatically.

Read-only

readonly renders the value as plain text — no border, stepper, or focus ring — for view modes that later switch to editing. Unlike disabled, it reads as content rather than a blocked control.

1250

Subtle variant

subtle drops the border and background so the field reads as inline text until hovered or focused. Use it for in-place editing in dense surfaces; keep default in forms.

Sizes

md is the default. Match the size of neighbouring inputs and buttons; use xxs–sm in dense chrome such as table rows and toolbars.

Widths

Width presets align fields across form rows: xs, sm, md, lg, plus fullWidth (the default, stretching to the container) and fitContent, which hugs the current value. Set a custom width with className when alignment doesn't matter.

Sizing to the maximum value

width="maxNumber" sizes the field to exactly fit max at step's precision. It assumes the stepper is hidden, so pair it with hideStepper, and it needs a finite max.

Hiding the stepper

The spin buttons show only while the field is focused but reserve their space; hideStepper removes them and reclaims it. Hide the stepper when values are typed rather than nudged.

Clearable

clearable shows a clear button on hover or focus; clearing reports null to onChange and refocuses the input. Pass { onClear } to run your own reset instead.

Units with prefix and suffix

Put the unit in an adornment so users enter only the number. Text adornments take { text }, icons { iconName }, and either becomes a button when given onClick.

MB
$

Formatted display

styledValue overlays a custom rendering while the field is unfocused — thousands separators, highlights — and reveals the raw number for editing on focus.

1,234,567

Connected inputs

Set connectToRightInput on the left input and connectToLeftInput on the right to join a pair into one visual control. Only the default variant connects — subtle and readonly inputs render separately.

In a form

Inside Form.Field the label connects to the input automatically. Keep invalid and the errors list in step so the message explains the red border.

import { Form, NumberInput } from "@hashintel/ds-components";

export const MaxTokensField = ({
  value,
  error,
  onChange,
}: {
  value: number | null;
  error?: string;
  onChange: (value: number | null) => void;
}) => (
  <Form.Field
    label="Max tokens"
    description="Upper limit for each flow step"
    errors={error ? [error] : undefined}
  >
    <NumberInput
      value={value}
      onChange={onChange}
      min={1}
      max={128000}
      width="sm"
      invalid={!!error}
    />
  </Form.Field>
);

Behavior

  • An integer step makes an integer input: decimal-separator and exponent keys are blocked with a brief grey flash, and parsed values are truncated. A fractional step (or "any") accepts every locale's decimal separator.
  • Typed values are never clamped: arrows and spin buttons stay within min/max, but typing can leave the range. Validate the value and set invalid.
  • Scrolling over a focused field doesn't change the value — the wheel is disabled on focus to prevent accidental edits.
  • Spin buttons appear on focus and reserve their space while hidden, so the layout never shifts; Firefox never shows them.
  • The minimum width fits two digits plus padding — much narrower than other inputs — so number fields sit comfortably in table rows.

Content

  • Name the quantity in the label and put the unit in a suffix adornment: "Memory limit" plus "MB", not "Memory limit in megabytes".
  • Use the placeholder for an example value ("e.g. 512"), never as the label — it disappears as soon as users type.
  • Put the valid range in the field description, and template errors as "[Field] must be between [min] and [max]": "Batch size must be between 1 and 512."

Accessibility

NumberInput renders a native <input type="number">, which implements the WAI-ARIA spinbutton pattern — arrow-key stepping and numeric semantics come from the browser.

What you must do

  • Always label the input — via Form.Field, your own label with htmlForId, or aria-label.
  • Pair invalid with a visible message (for example Form.Field errors) — color alone doesn't explain the problem.
  • Surface the valid range in the label or description — rejected keystrokes and stepping limits only flash the field, they don't announce why.

API reference

NumberInput

PropTypeDescription
value
number |⁠ null |⁠ undefined
onChange
(value: number |⁠ null, event: ChangeEvent<HTMLInputElement, Element>) => void
onClick
MouseEventHandler<Element>
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "md"
placeholder
string
step
number |⁠ "any"
default: 1
max
number
default: Number.MAX_SAFE_INTEGER
prefix
PrefixOrSuffix
suffix
PrefixOrSuffix
variant
"subtle" |⁠ "default"
default: "default"
loading
boolean
disabled
boolean
tabIndex
number
autoFocus
boolean |⁠ "never"
onKeyDown
KeyboardEventHandler<Element>
onFocus
FocusEventHandler<HTMLInputElement>
onBlur
FocusEventHandler<HTMLInputElement>
min
number
default: 0
required
boolean
name
string
invalid
boolean
inputMode
"search" |⁠ "text" |⁠ "none" |⁠ "email" |⁠ … 4 more
readonly
boolean
align
"left" |⁠ "right" |⁠ "center"
default: "left"
connectToLeftInput
boolean
connectToRightInput
boolean
styledValue
ReactNode
clearable
boolean |⁠ { onClear: () => void; }
showEditIcon
boolean
autocomplete
boolean
default: false
pattern
string
width
"xs" |⁠ "sm" |⁠ "md" |⁠ "lg" |⁠ "fullWidth" |⁠ "fitContent" |⁠ "maxNumber"
default: "fullWidth"
hideStepper
boolean

PrefixOrSuffix

The object shape prefix and suffix take.

PropTypeDescription
iconName
"function" |⁠ "filter" |⁠ "at" |⁠ "circle" |⁠ … 137 more
text
string
content
ReactNode
onClick
(() => void) |⁠ (() => void)
disabled
boolean
variant
"subtle" |⁠ "default"
default: "default"

Was this page helpful?

On this page