# InputNumber

## Summary

`InputNumber` collects a single numeric value in a form. Reach for it when the
value benefits from being nudged up or down, such as quantities, prices,
durations, or counts.

Most fields only need the props shown below. For rare layouts the props can't
handle, you can build the same field from smaller pieces. See the **Implement**
tab.

### When to use

* The value is a number the user increments or decrements, like a quantity,
  price, day count, or number of repetitions
* A stepper, min/max bounds, or number formatting would help the user

### When not to use

* The value is a sequence of digits that is never calculated with, such as phone
  numbers, credit-card numbers, or postal codes. A stepper adds no value there;
  use [InputText](../InputText/InputText.md) instead.

## Anatomy

`InputNumber` typically includes:

* Label (required): names the value the field collects
* Value field (required): the number the user types or steps through
* Stepper (optional): increment and decrement controls that change the value by
  `step`
* Prefix or suffix (optional): a unit or symbol shown alongside the value, like
  $ or kg
* Loading indicator (optional): replaces the stepper while background work runs

## Behavior

* The field is controlled through `value`, where a number sets the value and
  `null` leaves it empty.
* `onValueCommitted` fires when the user commits a value: on blur, on Enter, or
  when they use the stepper or arrow keys. Use `onValueChange` for per-keystroke
  updates.
* The stepper buttons and the Up and Down arrow keys change the value by `step`
  (default 1).
* `min` and `max` bound the value, and the stepper stops at each limit.
* `loading` hides the stepper and shows an indicator in its place. The field
  stays editable, so use `readOnly` or `disabled` to lock it.

## Options

### Basic

Pass `label`, a controlled `value`, and `onValueCommitted`. Bounds (`min` /
`max`) and `step` are optional.

```tsx
import React, { useState } from "react";
import type { InputNumberProps } from "@jobber/components";
import { InputNumber } from "@jobber/components";

export function InputNumberBasicExample(props: Partial<InputNumberProps>) {
  const [value, setValue] = useState<number | null>(3);

  return (
    <InputNumber
      label="Quantity"
      min={0}
      max={100}
      {...props}
      value={value}
      onValueCommitted={setValue}
    />
  );
}
```

### Prefixes and suffixes

Use `prefix` or `suffix` to add a unit or symbol. A suffix can be a label, an
icon, or a clickable icon that runs an action (give it an `ariaLabel`).

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";
import { Content } from "@jobber/components/Content";

export function InputNumberAffixesExample() {
  const [price, setPrice] = useState<number | null>(42);
  const [days, setDays] = useState<number | null>(7);
  const [reps, setReps] = useState<number | null>(3);

  return (
    <Content>
      <InputNumber
        label="Price"
        prefix={{ label: "$" }}
        suffix={{ label: "USD" }}
        value={price}
        onValueCommitted={setPrice}
      />

      <InputNumber
        label="Follow-up in"
        suffix={{ icon: "calendar", label: "days" }}
        value={days}
        onValueCommitted={setDays}
      />

      <InputNumber
        label="Repetitions"
        suffix={{
          icon: "cross",
          ariaLabel: "Clear value",
          onClick: () => setReps(null),
        }}
        value={reps}
        onValueCommitted={setReps}
      />
    </Content>
  );
}
```

### Sizes

3 sizes are available. `default` fits almost every form; use `small` only in
tight spaces and `large` only in especially spacious layouts.

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";
import { Content } from "@jobber/components/Content";

export function InputNumberSizesExample() {
  const [small, setSmall] = useState<number | null>(42);
  const [base, setBase] = useState<number | null>(42);
  const [large, setLarge] = useState<number | null>(42);

  return (
    <Content>
      <InputNumber
        label="Small"
        size="small"
        suffix={{ label: "items" }}
        value={small}
        onValueCommitted={setSmall}
      />
      <InputNumber
        label="Default"
        size="default"
        suffix={{ label: "items" }}
        value={base}
        onValueCommitted={setBase}
      />
      <InputNumber
        label="Large"
        size="large"
        suffix={{ label: "items" }}
        value={large}
        onValueCommitted={setLarge}
      />
    </Content>
  );
}
```

### Formatting

`format` takes any `Intl.NumberFormatOptions` and controls only how the value is
displayed; the committed value stays a plain number. See the **Implement** tab
for how percent and currency values map to the underlying number.

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";
import { Content } from "@jobber/components/Content";

export function InputNumberFormattingExample() {
  const [currency, setCurrency] = useState<number | null>(1234.5);
  const [percent, setPercent] = useState<number | null>(0.5);
  const [decimal, setDecimal] = useState<number | null>(11.13);

  return (
    <Content>
      <InputNumber
        label="Currency"
        description='{ style: "currency", currency: "USD" }'
        format={{ style: "currency", currency: "USD" }}
        value={currency}
        onValueCommitted={setCurrency}
      />
      <InputNumber
        label="Percent"
        description='{ style: "percent" } — value is a ratio: 0.5 → 50%'
        format={{ style: "percent", maximumFractionDigits: 2 }}
        value={percent}
        onValueCommitted={setPercent}
      />
      <InputNumber
        label="Decimal"
        description="{ maximumFractionDigits: 2 }"
        format={{ maximumFractionDigits: 2 }}
        value={decimal}
        onValueCommitted={setDecimal}
      />
    </Content>
  );
}
```

### Loading

`loading` shows a non-blocking indicator in the stepper's slot for background
work, like saving. The field stays editable and the stepper is hidden while
loading.

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberLoadingExample() {
  const [value, setValue] = useState<number | null>(42);

  return (
    <InputNumber
      loading
      label="Quantity"
      suffix={{ label: "items" }}
      value={value}
      onValueCommitted={setValue}
    />
  );
}
```

## Content guidelines

### Label the unit, don't repeat it

Put the unit in the label or an affix, not both.

| ✅ Do                            | ❌ Don't                                 |
| ------------------------------- | --------------------------------------- |
| Label "Weight", suffix "kg"     | Label "Weight (kg)", suffix "kg"        |
| Label "Duration", suffix "days" | Label "Duration in days", suffix "days" |

### Keep labels short and sentence case

| ✅ Do     | ❌ Don't                 |
| -------- | ----------------------- |
| Quantity | Enter the quantity here |
| Discount | DISCOUNT %              |

### Put the symbol where it's read

Use a prefix for a leading symbol and a suffix for a trailing unit, matching how
the value is spoken.

| ✅ Do                 | ❌ Don't              |
| -------------------- | -------------------- |
| Prefix "$", value 40 | Suffix "$", value 40 |
| Suffix "%", value 15 | Prefix "%", value 15 |

### Keep validation errors helpful

When a value breaks `min` or `max`, provide helpful guidance on what values will
be accepted as opposed to just providing a generic error.

| ✅ Do                           | ❌ Don't       |
| ------------------------------ | ------------- |
| Enter a value between 1 and 99 | Invalid input |
| Quantity can't be more than 50 | Error         |

### Use numbers as opposed to spelling them

Use numerals in labels, helper text, affixes, and bounds.

| ✅ Do        | ❌ Don't         |
| ----------- | --------------- |
| Max 3 items | Max three items |

## Do's and Don'ts

#### Do:

* ✅ Use for values the user increments or decrements
* ✅ Set `min` and `max` when the value has real bounds
* ✅ Use `format` for currency, percent, and decimals rather than formatting the
  value yourself
* ✅ Use `loading` for background work so the field stays usable

#### Don't:

* ❌ Use it for digit sequences that are never calculated with, like phone or
  credit card numbers
* ❌ Disable the field to communicate an error; show an `error` message instead
* ❌ Repeat the unit in both the label and an affix

## Accessibility notes

The field is a native number input, so it is reachable and operable by keyboard
and assistive technology.

| Key              | Behavior                        |
| ---------------- | ------------------------------- |
| Tab              | Moves focus to the field        |
| Up / Down arrows | Increment / decrement by `step` |
| Enter            | Commits the current value       |
| Type             | Replaces the value              |

Give a clickable affix a clear `ariaLabel` describing its action, like "Clear
value".

## Related components

* For digit sequences that are not calculated with, like phone or credit card
  numbers, use [InputText](../InputText/InputText.md).
* For dates, use [InputDate](../InputDate/InputDate.md).


## Anatomy

The prop-driven `<InputNumber>` composes a set of parts. You only need these
when the props can't express a layout; otherwise reach for the props shown on
the **Design** tab.

| Part                    | Description                                                          |
| ----------------------- | -------------------------------------------------------------------- |
| `Wrapper`               | Owns the field configuration and state; provides it to the parts     |
| `Group`                 | The bordered field row                                               |
| `Input`                 | The input area; holds the `Label` and the `Stepper` / `Loading` slot |
| `Label`                 | Floating field label                                                 |
| `Stepper`               | The increment / decrement button pair                                |
| `Increment` `Decrement` | The individual stepper buttons                                       |
| `Affix`                 | Prefix / suffix content (label, icon, or clickable icon)             |
| `Loading`               | Non-blocking loading indicator slot                                  |
| `Footer`                | Below-field row that holds `Description` and `Error`                 |
| `Description`           | Helper text below the field                                          |
| `Error`                 | Styled error message below the field                                 |

`Wrapper` owns the field state and shares it with the parts through context, so
every part must be rendered inside a `Wrapper` (a part used outside one throws).

## Customize one part

Pass the content parts you want — `Label`, `Affix`, `Stepper`, `Loading`,
`Description`, `Error` — straight to `Wrapper`. It builds `Group`, `Input`, and
`Footer` around them and places each part at the right depth, so you never
reproduce the skeleton to change one piece. `Wrapper` renders only the parts you
gave it: leave one out and it is not there.

The example below replaces the default stepper icons with `+` / `−` and leaves
everything else as the default:

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberCompositionExample() {
  const [value, setValue] = useState<number | null>(3);

  return (
    <InputNumber.Wrapper onValueCommitted={setValue} value={value}>
      <InputNumber.Label>Quantity</InputNumber.Label>
      <InputNumber.Stepper>
        <InputNumber.Increment ariaLabel="Increase Quantity">
          +
        </InputNumber.Increment>
        <InputNumber.Decrement ariaLabel="Decrease Quantity">
          −
        </InputNumber.Decrement>
      </InputNumber.Stepper>
    </InputNumber.Wrapper>
  );
}
```

### Omit a part

A field with no stepper is the same composition minus the `Stepper`:

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberCompositionStepperlessExample() {
  const [value, setValue] = useState<number | null>(15);

  return (
    <InputNumber.Wrapper
      format={{ style: "unit", unit: "percent" }}
      onValueCommitted={setValue}
      value={value}
    >
      <InputNumber.Label>Tax rate</InputNumber.Label>
    </InputNumber.Wrapper>
  );
}
```

### Show a part conditionally

Presence is ordinary conditional JSX; there is no `show*` prop to reach for:

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberCompositionConditionalStepperExample() {
  const [unitCost, setUnitCost] = useState<number | null>(0);
  const [markup, setMarkup] = useState<number | null>(20);

  return (
    <>
      <InputNumber
        label="Unit cost"
        onValueCommitted={setUnitCost}
        prefix={{ label: "$" }}
        value={unitCost}
      />
      <InputNumber.Wrapper onValueCommitted={setMarkup} value={markup}>
        <InputNumber.Label>Markup</InputNumber.Label>
        <InputNumber.Affix label="%" variation="suffix" />
        {Boolean(unitCost) && <InputNumber.Stepper />}
      </InputNumber.Wrapper>
    </>
  );
}
```

### Below-field content

`Description` and `Error` route into the footer row, which exists only while one
of them is rendered. The two are never shown together: when both are present the
error replaces the description, since a description hints at what the field
wants and the error already restates it.

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberCompositionFooterExample() {
  const [value, setValue] = useState<number | null>(50);
  const error = (value ?? 0) > 99 ? "Enter a value between 1 and 99" : "";

  return (
    <InputNumber.Wrapper
      invalid={Boolean(error)}
      max={99}
      min={1}
      onValueChange={setValue}
      value={value}
    >
      <InputNumber.Label>Quantity</InputNumber.Label>
      <InputNumber.Description>Per visit</InputNumber.Description>
      {error && <InputNumber.Error>{error}</InputNumber.Error>}
    </InputNumber.Wrapper>
  );
}
```

## Custom layouts

For an arrangement the standard skeleton does not produce, compose the
structural parts (`Group`, `Input`, `Footer`) yourself. `Wrapper` then renders
your tree exactly as written and adds nothing:

```tsx
import React, { useState } from "react";
import { InputNumber } from "@jobber/components";

export function InputNumberCompositionCustomLayoutExample() {
  const [value, setValue] = useState<number | null>(3);

  return (
    <InputNumber.Wrapper inline onValueCommitted={setValue} value={value}>
      <InputNumber.Group>
        <InputNumber.Decrement ariaLabel="Decrease quantity" />
        <InputNumber.Input>
          <InputNumber.Label>Quantity</InputNumber.Label>
        </InputNumber.Input>
        <InputNumber.Increment ariaLabel="Increase quantity" />
      </InputNumber.Group>
    </InputNumber.Wrapper>
  );
}
```

Pick one path per `Wrapper`. Mixing structural parts with loose content parts
throws, rather than guessing where the loose parts belong.

The sugar does not merge consumer-provided parts into its render, so there is no
per-slot precedence to reason about — children passed to `<InputNumber>` are
ignored. Compose on `Wrapper` instead.

## Controlled usage

The field is controlled: pass `value` (a `number`, or `null` for empty) and read
changes back through one of two callbacks.

| Callback           | Fires                                                       | Use for                            |
| ------------------ | ----------------------------------------------------------- | ---------------------------------- |
| `onValueChange`    | On every parsed change (typing, paste, stepper, arrow step) | Live-updating UI as the user types |
| `onValueCommitted` | When the user commits (blur, Enter, stepper, arrow step)    | Saving / validating a final value  |

Both emit `null` when the field is empty. Prefer `onValueCommitted` for
persistence so you are not writing on every keystroke.

## Formatting semantics

`format` is forwarded to Base UI's `NumberField` `format` and accepts any
`Intl.NumberFormatOptions`. It changes the display only; the committed value is
always a plain number. Two things to know:

* **Percent** (`{ style: "percent" }`) treats the value as a ratio: `0.5`
  renders `50%`, and the stepper moves in ratio units. If you want the value to
  be the number itself (`50` → `50%`), use `{ style: "unit", unit: "percent" }`.
* With no `format`, typed decimals are preserved (up to 12 fractional digits)
  and thousands grouping follows the locale default, so `1234.5` renders as
  `1,234.5`. Pass `{ useGrouping: false }` to render without separators.


## Props

### Mobile

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `accessibilityHint` | `string` | No | — | An accessibility hint helps users understand what will happen when they perform an action on the accessibility elemen... |
| `accessibilityLabel` | `string` | No | — | VoiceOver will read this string when a user selects the associated element |
| `assistiveText` | `string` | No | — | Text that helps the user understand the input |
| `autoCapitalize` | `"characters" | "none" | "sentences" | "words"` | No | — | Determines where to autocapitalize |
| `autoComplete` | `"email" | "name" | "additional-name" | "address-line1" | "address-line2" | "birthdate-day" | "birthdate-full" | "birthdate-month" | "birthdate-year" | "cc-csc" | "cc-exp" | ... 45 more ... | "off"` | No | — | Determines which content to suggest on auto complete, e.g.`username`. Default is `off` which disables auto complete  ... |
| `autoCorrect` | `boolean` | No | — | Turn off autocorrect |
| `autoFocus` | `boolean` | No | — | Automatically focus the input after it is rendered |
| `clearable` | `Clearable` | No | — | Add a clear action on the input that clears the value.  You should always use `while-editing` if you want the input t... |
| `defaultValue` | `number` | No | — |  |
| `disabled` | `boolean` | No | — | Disable the input |
| `invalid` | `boolean | string` | No | — | Highlights the field red and shows message below (if string) to indicate an error |
| `keyboard` | `NumberKeyboard` | No | — |  |
| `loading` | `boolean` | No | — | Show loading indicator. |
| `loadingType` | `"glimmer" | "spinner"` | No | — | Change the type of loading indicator to spinner or glimmer. |
| `multiline` | `boolean` | No | — | Determines if inputText will span multiple lines. Default is `false`  https://reactnative.dev/docs/textinput#multiline |
| `name` | `string` | No | — | Name of the input. |
| `onBlur` | `(event?: FocusEvent) => void` | No | — | Callback that is called when the text input is blurred |
| `onChange` | `(newValue?: string | number) => void` | No | — |  |
| `onFocus` | `(event?: FocusEvent) => void` | No | — | Callback that is called when the text input is focused @param event |
| `onSubmitEditing` | `(event?: SyntheticEvent<Element, Event>) => void` | No | — | Callback that is called when the text input's submit button is pressed @param event |
| `placeholder` | `string` | No | — | Hint text that goes above the value once the field is filled out |
| `prefix` | `{ icon?: IconNames; label?: string; }` | No | — | Symbol to display before the text input |
| `readonly` | `boolean` | No | — | Makes the input read-only |
| `ref` | `Ref<InputTextRef>` | No | — | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (... |
| `secureTextEntry` | `boolean` | No | — | Use secure text entry |
| `showMiniLabel` | `boolean` | No | `true` | Controls the visibility of the mini label that appears inside the input when a value is entered. By default, the plac... |
| `spellCheck` | `boolean` | No | — | Determines whether spell check is used. Turn it off to hide empty autoCorrect suggestions when autoCorrect is off.  *... |
| `styleOverride` | `InputTextStyleOverride` | No | — | Custom styling to override default style of the input text |
| `suffix` | `{ icon?: IconNames; label?: string; onPress?: () => void; }` | No | — | Symbol to display after the text input |
| `testID` | `string` | No | — | Used to locate this view in end-to-end tests |
| `textContentType` | `"none" | "name" | "nickname" | "password" | "username" | "URL" | "addressCity" | "addressCityAndState" | "addressState" | "countryName" | "creditCardNumber" | "creditCardExpiration" | ... 33 more ... | "shipmentTrackingNumber"` | No | — | Determines which content to suggest on auto complete, e.g.`username`. Default is `none` which disables auto complete ... |
| `toolbar` | `ReactNode` | No | — | Add a toolbar below the input field for actions like rewriting the text. |
| `toolbarVisibility` | `"always" | "while-editing"` | No | — | Change the behaviour of when the toolbar becomes visible. |
| `transform` | `{ input?: (v: any) => string; output?: (v: string) => any; }` | No | — | transform object is used to transform the internal TextInput value It's useful for components like InputNumber where ... |
| `validations` | `RegisterOptions` | No | — | Shows an error message below the field and highlight the field red when value is invalid |
| `value` | `number` | No | — |  |
