Skip to main content

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 when free-form numeric text is required.

Basic usage#

import { Field, InputNumber } from 'moraine'

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

Playground#

Props
Slots

Anatomy#

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.

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.

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
↑ Increments the value by step.
↓ Decrements the value by step.
⇞ Increments by largeStep (defaults to step * 10).
⇟ Decrements by largeStep (defaults to step * 10).
↖ Sets the value to minValue when a minimum is defined.
↘ Sets the value to maxValue when a maximum is defined.
↵ Commits the current text input as a numeric value.

Examples#

Controlled value#

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#

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#

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#

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
data-disabledSlot: input-number, input-number-input, input-number-increment, input-number-decrementDescription: Present when the component, slot, or item is disabled.
data-invalidSlot: input-number, input-number-inputDescription: Present when the field or form has a validation error.
data-readonlySlot: input-number, input-number-inputDescription: Present when the field is in read-only mode.
data-requiredSlot: input-number, input-number-inputDescription: Present when the field input is required.
data-auto-alignSlot: input-number-inputDescription: Present when alignment is selected automatically.
data-activeSlot: input-number-increment, input-number-decrementDescription: Present when the item or trigger is active.

Props#

Props for the InputNumber component. Renders a <div> element by default.

Prop