NumberField
A field that allows users to enter a number and increment or decrement the value using stepper buttons.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField aria-label="Enter your favourite number" inputProps={{ placeholder: "Enter your favourite number", }} /> )}Usage
Use NumberField for numeric input with optional stepper buttons. Use TextField with type="tel" or a custom pattern if you need digit-only input without steppers.
import { NumberField } from "@opengovsg/oui"<NumberField aria-label="Enter quantity" />Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/number-field.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/number-field.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/number-field.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/number-field.jsonThe NumberField component provides an accessible way for users to enter numeric values with optional stepper buttons for incrementing and decrementing the value.
If the component does not have a visible label (by passing a label prop), an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.
Examples
With Label and Description
Provide clear instructions to users with labels and descriptions.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Quantity" description="Enter the quantity you want to order" /> )}With Placeholder
Use the inputProps.placeholder to show placeholder text.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Age" inputProps={{ placeholder: "Enter your age", }} /> )}Hide Steppers
Use the hideSteppers prop to hide the increment and decrement buttons.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Amount" hideSteppers inputProps={{ placeholder: "Enter amount", }} /> )}Start and End Content
Add custom content to the start or end of the input field using startContent and endContent props.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Price" defaultValue={100} startContent={ <div className="pointer-events-none flex items-center"> <span className="text-interaction-main-default">$</span> </div> } endContent={ <div className="flex items-center"> <label className="sr-only" htmlFor="currency"> Currency </label> <select aria-label="Select currency" className="text-interaction-main-default border-0 bg-transparent outline-transparent outline-solid" defaultValue="SGD" id="currency" name="currency" > <option aria-label="SG Dollar" value="SGD"> SGD </option> <option aria-label="US Dollar" value="USD"> USD </option> <option aria-label="Euro" value="EUR"> EUR </option> </select> </div> } /> )}Min and Max Values
Use minValue and maxValue props to restrict the range of acceptable values.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Quantity" description="Min: 0, Max: 100" minValue={0} maxValue={100} defaultValue={0} /> )}Step Value
Use the step prop to define the increment/decrement step size.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Volume" description="Adjust in increments of 5" step={5} defaultValue={50} /> )}Format Options
Use the formatOptions prop to format the number display with internationalization support.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-col gap-4"> <NumberField label="Currency" defaultValue={1000} formatOptions={{ style: "currency", currency: "USD", }} /> <NumberField label="Percentage" defaultValue={0.5} formatOptions={{ style: "percent", }} /> <NumberField label="Unit" defaultValue={10} formatOptions={{ style: "unit", unit: "kilogram", }} /> </div> )}Controlled
Use the value and onChange props to control the number field value programmatically.
import { useState } from "react"import { NumberField } from "@opengovsg/oui"export const Example = () => { const [value, setValue] = useState(25) return ( <div className="flex flex-col gap-4"> <NumberField label="Controlled Number Field" value={value} onChange={setValue} /> <p className="text-sm text-gray-600">Current value: {value}</p> </div> )}With Error Message
Combine isInvalid and errorMessage props to show validation errors.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Age" minValue={18} maxValue={100} defaultValue={15} isInvalid errorMessage="Age must be between 18 and 100" /> )}Sizes
Use the size prop to change the size of the number field.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-col gap-6"> <NumberField size="xs" label="Extra Small" inputProps={{ placeholder: "Extra small size" }} /> <NumberField size="sm" label="Small" inputProps={{ placeholder: "Small size" }} /> <NumberField size="md" label="Medium (default)" inputProps={{ placeholder: "Medium size" }} /> </div> )}Disabled
Use the isDisabled prop to disable the number field.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Quantity" isDisabled inputProps={{ placeholder: "This field is disabled", }} /> )}Read Only
Use the isReadOnly prop to make the number field read-only.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Order Total" isReadOnly defaultValue={150} description="This value cannot be changed" /> )}Required
Use the isRequired prop to mark the field as required.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Quantity" isRequired description="This field is required" /> )}Disable Wheel
By default, users can change the value using the mouse wheel. Use isWheelDisabled to prevent this.
import { NumberField } from "@opengovsg/oui"export const Example = () => { return ( <NumberField label="Price" isWheelDisabled defaultValue={100} description="Mouse wheel scrolling is disabled" /> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
The NumberField component supports built-in validation:
- Range validation: Automatically validates against
minValueandmaxValue - Required validation: Use
isRequiredto make the field mandatory - Custom validation: Use the
validateprop for custom validation logic
Accessibility
- Built with a native
<input type="number">element - Full keyboard support (arrow keys to increment/decrement, typing to enter values)
- Proper ARIA labeling and descriptions
- Required and invalid states exposed to assistive technology
- Stepper buttons are properly labeled for screen readers
Slots
- base: The root container wrapper
- label: The label text element
- field: The field group wrapper containing input and steppers
- input: The input element
- stepperContainer: Container for the stepper buttons
- increment: The increment button
- decrement: The decrement button
- description: The description text below the field
- error: The error message text
Data Attributes
NumberField has the following attributes on the base element, which you can use to style the component based on its state (e.g. group-[data-invalid=true]:bg-red-500):
- data-invalid: When the input is invalid. Based on
isInvalidprop. - data-required: When the input is required. Based on
isRequiredprop. - data-readonly: When the input is readonly. Based on
isReadOnlyprop. - data-focus: When the input is being focused.
- data-focus-within: When the input is being focused or any of its children.
- data-focus-visible: When the input is being focused with the keyboard.
- data-disabled: When the input is disabled. Based on
isDisabledprop. - data-hide-steppers: When the steppers are hidden. Based on
hideSteppersprop. - data-has-start-content: When the input has start content. Based on
startContentprop. - data-has-end-content: When the input has end content. Based on
endContentprop.
Custom Styles
You can customize the NumberField component by passing custom Tailwind CSS classes to the component slots via the classNames prop.
<NumberField
label="Price"
classNames={{
base: "max-w-xs",
input: "text-right",
label: "font-bold",
}}
/>Props
NumberField
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | The label for the number field |
description | string | - | The description text shown below the field |
errorMessage | string | ((validation: ValidationResult) => string) | - | The error message to display when validation fails |
value | number | - | The current value (controlled) |
defaultValue | number | - | The default value (uncontrolled) |
onChange | (value: number) => void | - | Callback fired when the value changes |
minValue | number | - | The minimum allowed value |
maxValue | number | - | The maximum allowed value |
step | number | 1 | The amount to increment/decrement when using steppers |
formatOptions | Intl.NumberFormatOptions | - | Formatting options for number display |
hideSteppers | boolean | false | Whether to hide the stepper buttons |
startContent | React.ReactNode | - | Content to display at the start of the input |
endContent | React.ReactNode | - | Content to display at the end of the input |
size | "xs" | "sm" | "md" | "md" | The size of the component |
isDisabled | boolean | false | Whether the field is disabled |
isReadOnly | boolean | false | Whether the field is read-only |
isRequired | boolean | false | Whether the field is required |
isInvalid | boolean | false | Whether the field should display as invalid |
isWheelDisabled | boolean | false | Whether to disable value changes via mouse wheel |
validate | (value: number) => ValidationError | true | null | undefined | - | Custom validation function |
inputProps | Partial<InputProps> | - | Additional props to pass to the input element |
classNames | SlotsToClasses<NumberFieldSlots> | - | Custom CSS classes for component slots |