---
title: Number Field
description: A numeric input field with increment and decrement buttons for precise value selection within a defined range.
---

```tsx
'use client';

import { NumberField } from '@/ui/forms/NumberField';

export function NumberFieldDemo() {
  return (
    <NumberField
      label="Quantity"
      description="Select the number of items"
      min={0}
      max={10}
      value={3}
      labels={{
        increase: 'Increase',
        decrease: 'Decrease',
        input: 'Input',
      }}
    />
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/number-field.json
```

Or manually, copy the following code into `/ui/forms/number-field.tsx`:

```tsx
import clsx from 'clsx';

import { type ChangeEvent, type MouseEvent, useId } from 'react';

import { useValue } from '../hooks/useValue';
import { Button } from '../buttons/Button';
import { FormControl, type FormControlProps } from './FormControl';

interface NumberFieldProps extends FormControlProps<number> {
  min?: number;
  max?: number;
  controlled?: boolean;
  labels: {
    increase: string;
    decrease: string;
    input: string;
  };
}

const INPUT_STYLE = { width: '28px', height: '28px' };

export function useNumberField(props: NumberFieldProps) {
  const internalId = useId();

  const {
    id = internalId,
    name = id,
    value: initialValue = 0,
    onChange,
    min = 0,
    max = 10,
    disabled = false,
    dataTestId = 'NumberField',
    inline = true,
    controlled = false,
    labels,
    ...rest
  } = props;

  const { value, setValue } = useValue<number, string | number | boolean>({
    ...(controlled ? { initialValue } : { defaultValue: initialValue }),
    formatter: Number,
    onChange,
  });

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const newValue = Number(e.target.value);

    if (newValue >= min && newValue <= max) {
      setValue(Number(newValue), e.nativeEvent);
    }
  };

  const increase = (event?: Event) => {
    if (value < max) {
      setValue(value + 1, event);
    }
  };

  const decrease = (event?: Event) => {
    if (value > min) {
      setValue(value - 1, event);
    }
  };

  return {
    ...rest,
    id,
    min,
    max,
    name,
    value,
    dataTestId,
    disabled,
    inline,
    labels,
    handleChange,
    increase,
    decrease,
  };
}

export const NumberField = (props: NumberFieldProps) => {
  const {
    disabled,
    min,
    max,
    label,
    description,
    id,
    dataTestId,
    value,
    handleChange,
    increase,
    decrease,
    errorMessage,
    validationStatus,
    hideRequiredStar,
    required,
    inline,
    labels,
    ...rest
  } = useNumberField(props);

  return (
    <FormControl
      id={id}
      label={label}
      description={description}
      className={clsx('flex items-center', props.className)}
      dataName="NumberField"
      dataTestId={dataTestId}
      errorMessage={errorMessage}
      validationStatus={validationStatus}
      disabled={disabled}
      required={required}
      hideRequiredStar={hideRequiredStar}
      layout="vertical"
      inline={inline}
    >
      <div className="flex items-center gap-x-8">
        <Button
          onClick={(e: MouseEvent<HTMLButtonElement>) => decrease(e.nativeEvent)}
          variant="circle"
          disabled={value <= min || disabled}
          icon="MinusDefault"
          aria-label={labels.decrease}
          data-testid={`${dataTestId}-decrease`}
        />
        <input
          {...rest}
          className={clsx(
            'text-b2 text-center font-bold outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',
            {
              'bg-pearl text-grey': disabled,
            },
          )}
          style={INPUT_STYLE}
          type="number"
          title={labels.input}
          id={id}
          onChange={handleChange}
          disabled={disabled}
          required={required}
          value={value}
          min={min}
          max={max}
        />
        <Button
          onClick={(e: MouseEvent<HTMLButtonElement>) => increase(e.nativeEvent)}
          variant="circle"
          aria-label={labels.increase}
          disabled={value >= max || disabled}
          icon="PlusDefault"
          data-testid={`${dataTestId}-increase`}
        />
      </div>
    </FormControl>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { NumberField } from '@/docs/lib/ui/forms/number-field';

export function Component() {
  const [quantity, setQuantity] = useState(3);

  return (
    <NumberField
      label="Quantity"
      description="Select the number of items"
      value={quantity}
      onChange={(_, value) => setQuantity(value)}
      min={0}
      max={10}
      required
    />
  );
}
```

## API Reference

### NumberField

| Prop               | Type                                    | Default         | Description                                                                                      |
| ------------------ | --------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------ |
| `label`            | `string`                                | `undefined`     | The label text displayed above the input field.                                                  |
| `description`      | `string`                                | `undefined`     | Optional helper text displayed below the label to provide additional context.                    |
| `id`               | `string`                                | Auto-generated  | Unique identifier for the input element. Auto-generated if not provided.                         |
| `name`             | `string`                                | Same as `id`    | The name attribute for the input field, used in form submissions. Defaults to the `id` value.    |
| `value`            | `number`                                | `0`             | The current value of the number field.                                                           |
| `onChange`         | `(event: Event, value: number) => void` | `undefined` | Callback invoked when value changes. Receives event and new numeric value. |
| `min`              | `number`                                | `0`             | The minimum allowed value for the input.                                                         |
| `max`              | `number`                                | `10`            | The maximum allowed value for the input.                                                         |
| `disabled`         | `boolean`                               | `false`         | If true, disables the input field and all buttons, preventing user interaction.                  |
| `required`         | `boolean`                               | `false`         | If true, marks the field as required with an asterisk indicator.                                 |
| `hideRequiredStar` | `boolean`                               | `false`         | If true, hides the asterisk indicator even when the field is required.                           |
| `validationStatus` | `"default" \| "error" \| "success"`     | `"default"`     | Visual state of the field indicating validation status.                                          |
| `errorMessage`     | `string`                                | `undefined`     | Error message displayed below the input when `validationStatus` is set to `"error"`.             |
| `dataTestId`       | `string`                                | `"NumberField"` | Test identifier for the component, used for testing purposes.                                    |
| `className`        | `string`                                | `undefined`     | Additional CSS classes to apply to the FormControl container.                                    |

The NumberField also supports all standard HTML input attributes via `ComponentPropsWithoutRef<"input">`.

## Notes

- **Range Validation**: The component automatically enforces the min/max range. Values entered directly in the input field are only accepted if they fall within the specified range.
- **Button Controls**: Plus and minus buttons provide an intuitive way to adjust values. These buttons are automatically disabled when the value reaches the min or max boundary.
- **Disabled State**: When disabled, the input field shows a grey background color (`bg-pearl text-grey`), and all interactive elements (input and buttons) become non-functional.
- **FormControl Integration**: NumberField uses the FormControl component internally, inheriting features like labels, descriptions, error messages, and validation states.
- **Automatic ID Generation**: If no `id` prop is provided, a unique ID is automatically generated using React's `useId` hook.
- **Value Management**: The component uses the internal `useValue` hook for state management, which handles value formatting and change callbacks.
- **Fixed Input Size**: The numeric input has a fixed width and height (28px × 28px) for consistent appearance.
- **Keyboard Support**: Users can type values directly into the input field, with validation applied on change.
- **Client Component**: This component must be used in a client component context (`"use client"`).
- **Accessibility**: Both increment and decrement buttons have appropriate `aria-label` attributes ("Increase" and "Decrease") for screen reader support.
- **Data Attributes**: The component includes `data-name="NumberField"` and `data-testid` attributes for testing and debugging.
