dotUI
dotUI
beta
  1. Components
  2. Inputs
  3. NumberField

NumberField

NumberField allows a user to enter a number, and increment or decrement the value using stepper buttons.

<NumberField label="Width" defaultValue={1024} />

Installation

Install the following dependencies:

npm install react-aria-components

Copy and paste the following code into your project.

"use client";

import * as React from "react";
import {
  NumberField as AriaNumberField,
  type NumberFieldProps as AriaNumberFieldProps,
} from "react-aria-components";
import { tv, type VariantProps } from "tailwind-variants";
import { useMediaQuery } from "@/lib/hooks/use-media-query";
import { ChevronDownIcon, ChevronUpIcon, MinusIcon, PlusIcon } from "@/lib/icons";
import { Button, type ButtonProps } from "./button";
import { Field, type FieldProps } from "./field";
import { InputRoot, Input, type inputStyles } from "./input";

const numberFieldStyles = tv({
  base: "flex flex-col gap-2 items-start w-48",
});

type NumberFieldProps = NumberFieldRootProps &
  Omit<FieldProps, "children"> &
  VariantProps<typeof inputStyles> & {
    placeholder?: string;
  };
const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
  (
    {
      className,
      size,
      placeholder,
      label,
      description,
      errorMessage,
      isRequired,
      necessityIndicator,
      contextualHelp,
      ...props
    },
    ref
  ) => {
    const isMobile = useMediaQuery("(max-width: 768px)");
    const prefix = isMobile ? (
      <NumberFieldButton slot="decrement" className="h-full rounded-none rounded-l-md border-r" />
    ) : null;
    const suffix = isMobile ? (
      <NumberFieldButton slot="increment" className="h-full rounded-none rounded-r-md border-l" />
    ) : (
      <div className="flex h-full flex-col rounded-r-md">
        <NumberFieldButton
          slot="increment"
          iconType="chevron"
          className="h-auto flex-1 shrink rounded-none rounded-tr-md border-b border-l"
        />
        <NumberFieldButton
          slot="decrement"
          iconType="chevron"
          className="h-auto flex-1 shrink rounded-none rounded-br-md border-l"
        />
      </div>
    );
    return (
      <NumberFieldRoot className={className} {...props}>
        <Field
          label={label}
          description={description}
          errorMessage={errorMessage}
          isRequired={isRequired}
          necessityIndicator={necessityIndicator}
          contextualHelp={contextualHelp}
        >
          <InputRoot
            size={size}
            className="px-0 [&_button]:invalid:border-border-danger [&_button]:focus-within:invalid:border-border"
          >
            {prefix}
            <Input ref={ref} placeholder={placeholder} className="px-2" />
            {suffix}
          </InputRoot>
        </Field>
      </NumberFieldRoot>
    );
  }
);
NumberField.displayName = "NumberField";

type NumberFieldRootProps = Omit<AriaNumberFieldProps, "className"> & {
  className?: string;
};
const NumberFieldRoot = React.forwardRef<
  React.ElementRef<typeof AriaNumberField>,
  NumberFieldRootProps
>(({ className, ...props }, ref) => {
  return <AriaNumberField ref={ref} className={numberFieldStyles({ className })} {...props} />;
});
NumberFieldRoot.displayName = "NumberFieldRoot";

interface NumberFieldButtonProps extends ButtonProps {
  slot: "increment" | "decrement";
  iconType?: "chevron" | "default";
}
const NumberFieldButton = ({ slot, iconType = "default", ...props }: NumberFieldButtonProps) => {
  const icon =
    iconType === "chevron" ? (
      slot === "increment" ? (
        <ChevronUpIcon />
      ) : (
        <ChevronDownIcon />
      )
    ) : slot === "increment" ? (
      <PlusIcon />
    ) : (
      <MinusIcon />
    );
  return (
    <Button slot={slot} size="sm" variant="quiet" shape="square" {...props}>
      {icon}
    </Button>
  );
};

export type { NumberFieldProps, NumberFieldRootProps };
export { NumberField, NumberFieldRoot, NumberFieldButton };

Update the import paths to match your project setup.

Usage

Use a NumberField to allow a user to enter a number.

Options

Number formatting

The NumberField value can be formatted by using the formatOptions prop. formatOptions is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale.

<NumberField
  label="Decimal"
  defaultValue={0}
  formatOptions={{
    signDisplay: "exceptZero",
    minimumFractionDigits: 1,
    maximumFractionDigits: 2,
  }}
/>
<NumberField
  label="Percentage"
  defaultValue={0.05}
  formatOptions={{
    style: "percent",
  }}
/>
<NumberField
  label="Currency"
  defaultValue={45}
  formatOptions={{
    style: "currency",
    currency: "EUR",
    currencyDisplay: "code",
    currencySign: "accounting",
  }}
/>
<NumberField
  label="Unit"
  defaultValue={4}
  formatOptions={{
    style: "unit",
    unit: "inch",
    unitDisplay: "long",
  }}
/>

Size

Use the size prop to control the size of the NumberField. The default size is "md".

<NumberField size="sm" />
<NumberField size="md" />
<NumberField size="lg" />

Label

A visual label can be provided for the NumberField using the label prop or a hidden label using aria-label prop.

<NumberField label="Width" />
<NumberField aria-label="Width" />

Description

A description can be supplied to a NumberField via the description prop. The description is always visible unless the validationState is “invalid” and an error message is provided.

Enter the desired width.
<NumberField label="Width" description="Enter the desired width." />

Contextual help

A ContextualHelp element may be placed next to the label to provide additional information or help about a NumberField.

<NumberField label="Width" contextualHelp={<ContextualHelp />} />

Error message

An errorMessage can be supplied to a NumberField, which will be displayed when the isInvalid prop is set to true.

Please fill out this field.
<NumberField
  label="Width"
  isInvalid
  errorMessage="Please fill out this field."
/>

Disabled

Use the isDisabled prop to disable the NumberField.

<NumberField isDisabled />

ReadOnly

The isReadOnly boolean prop makes the NumberField's text content immutable. Unlike isDisabled, the NumberField remains focusable and the contents can still be copied.

<NumberField isReadOnly />

Required

Use the isRequired prop to mark the NumberField as required. Use the necessityIndicator prop to control the visual style of the required state.

<NumberField label="Width" isRequired />
<NumberField label="Width" isRequired necessityIndicator="icon" />
<NumberField label="Width" isRequired necessityIndicator="label" />
<NumberField label="Width" necessityIndicator="label" />

Uncontrolled

Use the defaultValue prop to set the default input value.

<NumberField defaultValue={80} />

Controlled

Use the value and onChange props to control the value of the input.

mirrored number: 69

const [value, setValue] = React.useState(69);
return <NumberField value={value} onChange={setValue} />;

Composition

If you need to customize things further, you can drop down to the composition level.

Enter the disired quantity.
<NumberFieldRoot>
  <Label>Quantity</Label>
  <Group className="flex items-center gap-2">
    <Button slot="decrement">
      <MinusIcon />
    </Button>
    <InputRoot>
      <Input />
    </InputRoot>
    <Button slot="increment">
      <PlusIcon />
    </Button>
  </Group>
  <Description>Enter the disired quantity.</Description>
</NumberFieldRoot>

API Reference

PropTypeDefaultDescription
size
"sm" | "md" | "lg"
"md"
The size of the input.
label
ReactNode
The content to display as the label.
description
ReactNode
A description for the field. Provides a hint such as specific requirements for what to choose.
errorMessage
ReactNode | (v: ValidationResult) => ReactNode
An error message for the field.
decrementAriaLabel
string
-
A custom aria-label for the decrement button. If not provided, the localized string "Decrement" is used.
incrementAriaLabel
string
-
A custom aria-label for the increment button. If not provided, the localized string "Increment" is used.
isWheelDisabled
boolean
-
Enables or disables changing the value with scroll.
formatOptions
Intl.NumberFormatOptions
-
Formatting options for the value displayed in the number field. This also affects what characters are allowed to be typed by the user.
isDisabled
boolean
-
Whether the input is disabled.
isReadOnly
boolean
-
Whether the input can be selected but not changed by the user.
isRequired
boolean
-
Whether user input is required on the input before form submission.
isInvalid
boolean
-
Whether the value is invalid.
validate
(value: string) => ValidationError | true | null | undefined
-
A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if validationBehavior="native". For realtime validation, use the isInvalid prop instead.
autoFocus
boolean
-
Whether the element should receive focus on render.
value
number
-
The current value (controlled).
defaultValue
number
-
The default value (uncontrolled).
minValue
number
-
The smallest value allowed for the input.
maxValue
number
-
The largest value allowed for the input.
step
number
-
The amount that the input value changes with each increment or decrement "tick".
validationBehavior
'native' | 'aria'
'native'
Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA.
name
string
-
The name of the input element, used when submitting an HTML form.
children
ReactNode | (values: NumberFieldRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode
-
The children of the component. A function may be provided to alter the children based on component state.
className
string
-
The CSS className for the element.
style
CSSProperties | (values: NumberFieldRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
EventTypeDescription
onFocus
(e: FocusEvent<Target>) => void
Handler that is called when the element receives focus.
onBlur
(e: FocusEvent<Target>) => void
Handler that is called when the element loses focus.
onFocusChange
(isFocused: boolean) => void
Handler that is called when the element's focus status changes.
onKeyDown
(e: KeyboardEvent) => void
Handler that is called when a key is pressed.
onKeyUp
(e: KeyboardEvent) => void
Handler that is called when a key is released.
onChange
(value: T) => void
Handler that is called when the value changes.
onCopy
ClipboardEventHandler<HTMLInputElement>
Handler that is called when the user copies text.
onCut
ClipboardEventHandler<HTMLInputElement>
Handler that is called when the user cuts text.
onPaste
ClipboardEventHandler<HTMLInputElement>
Handler that is called when the user pastes text.
onCompositionStart
CompositionEventHandler<HTMLInputElement>
Handler that is called when a text composition system starts a new text composition session.
onCompositionEnd
CompositionEventHandler<HTMLInputElement>
Handler that is called when a text composition system completes or cancels the current text composition session.
onCompositionUpdate
CompositionEventHandler<HTMLInputElement>
Handler that is called when a new character is received in the current text composition session.
onSelect
ReactEventHandler<HTMLInputElement>
Handler that is called when text in the input is selected.
onBeforeInput
FormEventHandler<HTMLInputElement>
Handler that is called when the input value is about to be modified.
onInput
FormEventHandler<HTMLInputElement>
Handler that is called when the input value is modified.
Data attributeDescription
[data-disabled]
Whether the text field is disabled.
[data-invalid]
Whether the text field is invalid.
dotUI
beta

Accessible, mobile friendly, modern UI components.

Built by mehdibha. The source code is available on GitHub.