# Number Field

A bounded numeric input with decrement and increment stepper buttons.

## Installation

```tsx
import { NumberField } from "@makeplane/propel/components/number-field";
```
## Usage

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { NumberField, numberFieldStepperSize } from "@makeplane/propel/components/number-field";
import { MinusOutline, AddOutline } from "@makeplane/propel/icons";
import * as React from "react";

export default function BasicDemo() {
  const [value, setValue] = React.useState<number | null>(2);

  return (
    <NumberField
      aria-label="Number of instances"
      size="lg"
      min={1}
      max={64}
      value={value}
      onValueChange={setValue}
      decrement={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Decrease"
          icon={<Icon icon={MinusOutline} />}
        />
      }
      increment={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Increase"
          icon={<Icon icon={AddOutline} />}
        />
      }
    />
  );
}
```
## Examples

### Sizes

The `size` axis sets the frame height (`sm` 24 / `md` 28 / `lg` 32). Pair each stepper `IconButton`
with `numberFieldStepperSize[size]` — button size is one rung below the field — so the ghost
controls sit flush inside the outer inset (1px border + 1px pad). No `xs` (20px) frame: grouped
controls omit halo rungs.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import {
  NumberField,
  type NumberFieldSize,
  numberFieldStepperSize,
} from "@makeplane/propel/components/number-field";
import { MinusOutline, AddOutline } from "@makeplane/propel/icons";

const SIZES: NumberFieldSize[] = ["sm", "md", "lg"];

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {SIZES.map((size) => (
        <NumberField
          key={size}
          aria-label={`Number of seats (${size})`}
          size={size}
          defaultValue={4}
          min={1}
          max={64}
          decrement={
            <IconButton
              variant="ghost"
              size={numberFieldStepperSize[size]}
              aria-label={`Decrease (${size})`}
              icon={<Icon icon={MinusOutline} />}
            />
          }
          increment={
            <IconButton
              variant="ghost"
              size={numberFieldStepperSize[size]}
              aria-label={`Increase (${size})`}
              icon={<Icon icon={AddOutline} />}
            />
          }
        />
      ))}
    </div>
  );
}
```

### Disabled

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { NumberField, numberFieldStepperSize } from "@makeplane/propel/components/number-field";
import { MinusOutline, AddOutline } from "@makeplane/propel/icons";

export default function DisabledDemo() {
  return (
    <NumberField
      aria-label="Number of seats"
      size="lg"
      defaultValue={12}
      min={1}
      max={64}
      disabled
      decrement={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Decrease"
          icon={<Icon icon={MinusOutline} />}
        />
      }
      increment={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Increase"
          icon={<Icon icon={AddOutline} />}
        />
      }
    />
  );
}
```

### Invalid

`NumberField` has no baked-in `error` prop. Wrap it in `Field invalid` (and optional `FieldError`)
so Base UI propagates `data-invalid` / `aria-invalid` to the input; the group border turns danger
via `:has([data-invalid])`.

```tsx
import { Field, FieldError, FieldLabel } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { NumberField, numberFieldStepperSize } from "@makeplane/propel/components/number-field";
import { MinusOutline, AddOutline } from "@makeplane/propel/icons";

export default function InvalidDemo() {
  return (
    <Field name="seats" invalid>
      <FieldLabel id="number-field-seats-label" size="lg" inset={false}>
        Number of seats
      </FieldLabel>
      <NumberField
        aria-labelledby="number-field-seats-label"
        size="lg"
        defaultValue={99}
        min={1}
        max={64}
        decrement={
          <IconButton
            variant="ghost"
            size={numberFieldStepperSize.lg}
            aria-label="Decrease"
            icon={<Icon icon={MinusOutline} />}
          />
        }
        increment={
          <IconButton
            variant="ghost"
            size={numberFieldStepperSize.lg}
            aria-label="Increase"
            icon={<Icon icon={AddOutline} />}
          />
        }
      />
      <FieldError size="lg" match={true}>
        Enter a value between 1 and 64.
      </FieldError>
    </Field>
  );
}
```

### Formatted

Pass `format` (an `Intl.NumberFormatOptions`) to format the displayed value — here as a currency amount stepped by 50.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { NumberField, numberFieldStepperSize } from "@makeplane/propel/components/number-field";
import { MinusOutline, AddOutline } from "@makeplane/propel/icons";
import * as React from "react";

export default function FormattedDemo() {
  const [value, setValue] = React.useState<number | null>(250);

  return (
    <NumberField
      aria-label="Monthly budget"
      size="lg"
      min={0}
      max={10000}
      step={50}
      value={value}
      onValueChange={setValue}
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      decrement={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Decrease"
          icon={<Icon icon={MinusOutline} />}
        />
      }
      increment={
        <IconButton
          variant="ghost"
          size={numberFieldStepperSize.lg}
          aria-label="Increase"
          icon={<Icon icon={AddOutline} />}
        />
      }
    />
  );
}
```

### Scrub

`NumberFieldScrubArea` is not baked into the ready-made. Assemble Base UI `NumberField.Root` with
propel's scrub + group parts (see Storybook **Components/NumberField → Scrub**). Drag the scrub
label horizontally to change the value:

```tsx
import { NumberField as BaseNumberField } from "@base-ui/react/number-field";
import {
  NumberFieldGroup,
  NumberFieldInput,
  NumberFieldScrubArea,
  NumberFieldScrubAreaCursor,
  numberFieldStepperSize,
} from "@makeplane/propel/components/number-field";
import { NumberField as NumberFieldFrame } from "@makeplane/propel/elements/number-field";

<BaseNumberField.Root defaultValue={24} render={<NumberFieldFrame />}>
  <NumberFieldScrubArea>
    <span className="cursor-ew-resize">Amount</span>
    <NumberFieldScrubAreaCursor />
  </NumberFieldScrubArea>
  <BaseNumberField.Group render={<NumberFieldGroup size="lg" />}>
    {/* Decrement / Input / Increment grafts — same as the ready-made */}
  </BaseNumberField.Group>
</BaseNumberField.Root>;
```
## API Reference

### NumberField

The ready-made number field: a numeric input flanked by decrement / increment buttons. Drive it
with `value`/`defaultValue` + `onValueChange`, bound it with `min`/`max`/`step`, and pass
`format` (an `Intl.NumberFormatOptions`) to format the displayed value. Name the input with
`aria-label` or `aria-labelledby` — the ready-made has no baked visible label.

Grafts Base UI `NumberField` behavior onto the `elements/number-field` styled parts
(`NumberField` root + `NumberFieldGroup` + `NumberFieldInput`) via `render`. The
`decrement`/`increment` steppers are consumer-provided controls, grafted onto Base UI's
`Decrement`/`Increment` behavior.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` (required) | `"sm" \| "md" \| "lg"` | — | Visual size of the field frame (`sm` 24 / `md` 28 / `lg` 32). Pair steppers with `numberFieldStepperSize[size]` — button size is one rung below the field. |
| `decrement` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The decrement control (e.g. an `IconButton`), rendered as the field's decrement stepper. It carries its own — localizable — `aria-label`; the field bakes no label or glyph. |
| `increment` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The increment control (e.g. an `IconButton`), rendered as the field's increment stepper. It carries its own — localizable — `aria-label`; the field bakes no label or glyph. |
| `aria-label` | `string` | — | Accessible name for the value input. Required when `aria-labelledby` is omitted. Accessible name for the value input. Optional when `aria-labelledby` is set. |
| `aria-labelledby` | `string` | — | Id of a visible label element. Optional when `aria-label` is set. Id of a visible label element. Required when `aria-label` is omitted. |

### NumberFieldScrubArea

Drag-to-change scrubbing over the field's label region — Base UI behavior, no chrome.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldScrubAreaState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### NumberFieldScrubAreaCursor

The virtual cursor shown while scrubbing; pass your cursor svg as children.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldScrubAreaCursorState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### NumberField

The styled number field frame — stacks the label, group, and messages. Base-UI-agnostic — graft
the number field behavior in `components` via `<BaseNumberField.Root render={<NumberField/>}
/>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### NumberFieldGroup

The styled bordered group wrapping the decrement / input / increment controls. Base-UI-agnostic —
graft in `components` via `<BaseNumberField.Group render={<NumberFieldGroup/>} />`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` (required) | `"sm" \| "md" \| "lg"` | — |  |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### NumberFieldInput

The styled numeric input. Base-UI-agnostic — graft the number field behavior in `components` via
`<BaseNumberField.Input render={<NumberFieldInput/>} />`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` (required) | `"sm" \| "md" \| "lg"` | — |  |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
