---
title: InputNumber
description: Edit a constrained number with step buttons and keyboard controls.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# InputNumber

> Edit a constrained number with step buttons and keyboard controls.

Use InputNumber for a numeric field with step controls and constrained committed values. Choose a plain [Input](https://moraine.subf.dev/components/input.md) when free-form numeric text is required.

## Basic usage

```tsx
import { Field, InputNumber } from 'moraine'

export function Example() {
  return (
    <Field label="Quantity">
      <InputNumber minValue={0} maxValue={10} defaultValue={1} />
    </Field>
  )
}
```

## Anatomy

```text
InputNumber [component; slot=root]
├── decrement [slot]
├── input [slot]
└── increment [slot]
```

## Usage

### Values and boundaries

Use `rawValue` with `onRawValueChange` when numeric state is application-owned; `value` and `onValueChange` expose the locale-formatted string. The component preserves partial typed input until commit, then applies `minValue` and `maxValue`. Decimal steps are calculated to avoid typical floating-point display noise.

```tsx
import { InputNumber } from 'moraine'
import { createSignal } from 'solid-js'

export function Boundaries() {
  const [value, setValue] = createSignal(5)

  return (
    <div class="max-w-xs w-full space-y-3">
      <InputNumber
        value={value()}
        onRawValueChange={(v) => {
          if (Number.isFinite(v)) {
            setValue(v)
          }
        }}
        minValue={0}
        maxValue={10}
      />
      <p class="text-xs text-muted-foreground">
        Numeric value: <span class="text-foreground font-mono">{value()}</span>
      </p>
    </div>
  )
}
```

### Stepping and repeat

In vertical layout, the input precedes an optional controls wrapper containing increment and decrement buttons. The input remains the spinbutton keyboard target.

`step` controls increment and decrement buttons, while `largeStep` controls PageUp and PageDown. Hold-to-repeat is enabled by default and can be tuned with the repeat timing props.

```tsx
import { InputNumber } from 'moraine'

export function Stepping() {
  return (
    <div class="flex flex-wrap gap-4 items-center">
      <InputNumber defaultValue={100} step={5} largeStep={25} aria-label="Step by 5 (PageUp: 25)" />
      <InputNumber
        defaultValue={0.5}
        step={0.1}
        minValue={0}
        maxValue={1}
        aria-label="Decimal step (0.1)"
      />
    </div>
  )
}
```

### Keyboard interaction

| Key                  | Description                                             |
| -------------------- | ------------------------------------------------------- |
| <kbd>ArrowUp</kbd>   | Increments the value by `step`.                         |
| <kbd>ArrowDown</kbd> | Decrements the value by `step`.                         |
| <kbd>PageUp</kbd>    | Increments by `largeStep` (defaults to `step * 10`).    |
| <kbd>PageDown</kbd>  | Decrements by `largeStep` (defaults to `step * 10`).    |
| <kbd>Home</kbd>      | Sets the value to `minValue` when a minimum is defined. |
| <kbd>End</kbd>       | Sets the value to `maxValue` when a maximum is defined. |
| <kbd>Enter</kbd>     | Commits the current text input as a numeric value.      |

## Examples

### Controlled value

```tsx
import { Badge, InputNumber } from 'moraine'
import { createSignal } from 'solid-js'

export function Controlled() {
  const [quantity, setQuantity] = createSignal(2)
  const unitPrice = 49.0

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-md space-y-4">
      <div class="flex items-center justify-between">
        <div>
          <h4 class="text-sm font-medium">Mechanical Keyboard (Pro Edition)</h4>
          <p class="text-xs text-muted-foreground">$49.00 / unit</p>
        </div>
        <Badge variant="outline">In Stock</Badge>
      </div>

      <div class="pt-2 border-t border-border flex items-center justify-between">
        <div class="space-y-1">
          <label class="text-xs text-muted-foreground font-medium">Quantity (Max 10)</label>
          <InputNumber
            value={quantity()}
            onRawValueChange={(v) => {
              if (Number.isFinite(v)) {
                setQuantity(v)
              }
            }}
            minValue={1}
            maxValue={10}
            step={1}
            variant="outline"
          />
        </div>

        <div class="text-right">
          <p class="text-xs text-muted-foreground">Subtotal</p>
          <p class="text-lg text-primary font-mono font-semibold">
            ${(quantity() * unitPrice).toFixed(2)}
          </p>
        </div>
      </div>
    </div>
  )
}
```

### Keyboard stepping

```tsx
import { InputNumber } from 'moraine'
import { createSignal } from 'solid-js'

export function KeyboardStepping() {
  const [value, setValue] = createSignal(4)

  return (
    <div class="max-w-sm space-y-3">
      <InputNumber
        aria-label="Retry delay in seconds"
        rawValue={value()}
        minValue={0}
        maxValue={60}
        step={0.5}
        largeStep={5}
        onRawValueChange={setValue}
      />
      <p class="text-sm text-muted-foreground">Current delay: {value()} seconds</p>
      <p class="text-xs text-muted-foreground">
        Arrow keys change by 0.5, PageUp/PageDown by 5, and Home/End jump to 0 or 60.
      </p>
    </div>
  )
}
```

### Press and hold

```tsx
import { InputNumber } from 'moraine'
import { createSignal } from 'solid-js'

export function LongPress() {
  const [repeatValue, setRepeatValue] = createSignal(12)
  const [singleStepValue, setSingleStepValue] = createSignal(12)

  return (
    <div class="max-w-xs space-y-4">
      <div class="space-y-2">
        <InputNumber
          value={repeatValue()}
          onRawValueChange={(value) => {
            if (Number.isFinite(value)) {
              setRepeatValue(value)
            }
          }}
          minValue={0}
          maxValue={99}
          step={1}
          variant="subtle"
        />
        <p class="text-xs text-muted-foreground">
          Hold <span class="font-medium">+</span> or <span class="font-medium">−</span> to repeat.
          Current value: {repeatValue()}
        </p>
      </div>

      <div class="space-y-2">
        <InputNumber
          value={singleStepValue()}
          holdRepeat={false}
          onRawValueChange={(value) => {
            if (Number.isFinite(value)) {
              setSingleStepValue(value)
            }
          }}
          minValue={0}
          maxValue={99}
          step={1}
          variant="subtle"
        />
        <p class="text-xs text-muted-foreground">
          Set <code>holdRepeat=false</code> to keep press-and-hold at a single step. Current value:{' '}
          {singleStepValue()}
        </p>
      </div>
    </div>
  )
}
```

### Form integration

```tsx
import { Button, createForm, InputNumber } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

export function FormIntegration() {
  const [submittedQuantity, setSubmittedQuantity] = createSignal<number | null>(null)
  const form = createForm({
    schema: v.object({
      quantity: v.pipe(
        v.number('Please enter a number'),
        v.minValue(1, 'Quantity must be at least 1 item.'),
        v.maxValue(10, 'Maximum order quantity is 10 items.'),
      ),
    }),
    initialInput: { quantity: 1 },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmittedQuantity(output.quantity)}>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="quantity"
          label="Order Quantity"
          description="Specify between 1 and 10 units."
          required
        >
          <InputNumber minValue={1} maxValue={10} />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">Selected units: {submittedQuantity() ?? 1}</p>
        </div>
      </div>
    </form.Form>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `input-number`, `input-number-input`, `input-number-increment`, `input-number-decrement` | Present when the component, slot, or item is disabled. |
| `data-invalid` | `input-number`, `input-number-input` | Present when the field or form has a validation error. |
| `data-readonly` | `input-number`, `input-number-input` | Present when the field is in read-only mode. |
| `data-required` | `input-number`, `input-number-input` | Present when the field input is required. |
| `data-auto-align` | `input-number-input` | Present when alignment is selected automatically. |
| `data-active` | `input-number-increment`, `input-number-decrement` | Present when the item or trigger is active. |

## Props

Props for the InputNumber component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| align | 'center' \| 'start' \| undefined | — | Text alignment; omitted values follow the control layout. |
| autofocus | boolean \| undefined | false | Whether to automatically focus the input on mount. |
| autofocusDelay | number \| undefined | 0 | Delay in milliseconds before focusing the input. |
| decrement | boolean \| undefined | true | Whether to show the decrement button. |
| decrementDisabled | boolean \| undefined | — | Whether the decrement button is disabled. |
| decrementIcon | IconT.Name \| undefined | orientation === 'vertical' ? 'icon-chevron-down' : 'icon-minus' | Icon for the decrement button. |
| defaultValue | string \| number \| undefined | — | Default displayed value for uncontrolled usage. |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| form | string \| undefined | — | The ID of an external form that owns the native spinbutton. |
| holdRepeat | boolean \| undefined | true | Whether press-and-hold should trigger repeated value changes. |
| id | string \| undefined | — | The ID of the input element. |
| increment | boolean \| undefined | true | Whether to show the increment button. |
| incrementDisabled | boolean \| undefined | — | Whether the increment button is disabled. |
| incrementIcon | IconT.Name \| undefined | orientation === 'vertical' ? 'icon-chevron-up' : 'icon-plus' | Icon for the increment button. |
| inputRef | Ref<HTMLInputElement> \| undefined | — | Optional inner input element ref. |
| largeStep | number \| undefined | step * 10 | The step size used for PageUp/PageDown. |
| locale | string \| undefined | — | Locale for number formatting and parsing.<br>Uses browser default if not specified. |
| maxValue | number \| undefined | — | Maximum allowed numeric value. |
| minValue | number \| undefined | — | Minimum allowed numeric value. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onBlur | JSX.FocusEventHandlerUnion<HTMLInputElement, FocusEvent> \| undefined | — | Callback when the input loses focus. |
| onDecrementClick | JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> \| undefined | — | Callback when the decrement button is clicked. |
| onFocus | JSX.FocusEventHandlerUnion<HTMLInputElement, FocusEvent> \| undefined | — | Callback when the input gains focus. |
| onIncrementClick | JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> \| undefined | — | Callback when the increment button is clicked. |
| onRawValueChange | ((value: number) => void) \| undefined | — | Callback when the numeric value changes. |
| onValueChange | ((value: string) => void) \| undefined | — | Callback when the formatted string value changes. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'horizontal' | Visual layout direction. |
| placeholder | string \| undefined | — | Placeholder text for the input. |
| rawValue | number \| undefined | — | Controlled numeric value. Takes precedence over `value`. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| repeatDelayMs | number \| undefined | 500 | Delay in milliseconds before repeated value changes start. |
| repeatIntervalMs | number \| undefined | 80 | Interval in milliseconds between repeated value changes. |
| repeatPointerTypes | 'all' \| PointerType \| undefined | 'all' | Pointer types that can trigger press-and-hold repeat. |
| repeatThrottleMs | number \| undefined | 0 | Minimum elapsed time in milliseconds between repeat triggers. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| step | number \| undefined | 1 | The increment/decrement step size. |
| value | string \| number \| undefined | — | Controlled displayed value. |
| variant | 'outline' \| 'subtle' \| 'ghost' \| 'none' \| undefined | 'outline' | Visual treatment of the component. |
| wheel | boolean \| undefined | false | Whether mouse wheel changes the value while the input is focused. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
