---
title: Number Input
description: Numeric input with stepper buttons
visibility: guest
draft: false
---

# Number Input

The `<x-wirekit::number-input>` component wraps a native `<input type="number">` with custom increment/decrement stepper buttons. Native browser spinners are hidden in favor of styled `+` and `-` buttons that match the WireKit design system.

## Basic Usage

:::preview{title="Basic Number Input"}
<x-wirekit::number-input label="Quantity" name="quantity" :value="1" :min="0" :max="99" />
:::

## With Step

:::preview{title="Quantity in packaging units"}
<x-wirekit::number-input label="Quantity in packaging units" name="quantity" :value="10" :min="0" :max="1000" :step="5" prefix="QTY" />
:::

## Decimal Input

Pass a fractional `step` to allow decimal entry. Common values: `step="0.01"` for currency / two-decimal precision, `step="0.1"` for measurements with one decimal, `step="0.001"` for engineering precision.

:::preview{title="Currency input — two decimal places"}
<x-wirekit::number-input label="Tax rate" name="tax_rate" :value="19.00" :min="0" :max="100" :step="0.01" suffix="%" />
:::

:::preview{title="Measurement — one decimal place"}
<x-wirekit::number-input label="Height" name="height" :value="1.78" :min="0.5" :max="2.5" :step="0.1" suffix="m" />
:::

### Decimal separator: dot vs comma

The native `<input type="number">` always **submits** form values using the dot notation (`19.99`, never `19,99`) — this is the W3C contract and is consistent across every browser. Backend code reads `request('tax_rate')` as a dot-formatted string regardless of the user's locale.

For **typing**, modern browsers respect the user's OS / browser locale:

- An English-locale user typing into the input accepts `19.99`; typing `19,99` is rejected.
- A German / French / Italian locale user typing accepts BOTH `19.99` AND `19,99`. The browser internally normalizes the comma to a dot before submitting.
- Mobile keyboards on iOS / Android show the appropriate decimal separator on the numeric keypad based on the device locale.

If you need stricter control over the displayed decimal separator (for example, always-comma display in a German-only app even when a user's browser locale differs), wrap the value in a Livewire computed property and format with PHP's `number_format($value, 2, ',', '.')` before binding to a regular text input. The `number-input` stepper UI is locale-agnostic at the input level by design — it follows browser convention.

## With Prefix & Suffix

:::preview{title="Prefix and Suffix"}
<x-wirekit::number-input label="Weight" name="weight" :value="50" suffix="kg" />

<x-wirekit::number-input label="Discount" name="discount" :value="15" suffix="%" :min="0" :max="100" />
:::

## Size Variants

:::preview{title="Three Sizes"}
<x-wirekit::number-input name="n-sm" label="Small" size="sm" :value="5" :min="1" :max="99" />

<x-wirekit::number-input name="n-md" label="Medium" size="md" :value="10" :min="1" :max="999" />

<x-wirekit::number-input name="n-lg" label="Large" size="lg" :value="15" :min="1" :max="9999" />
:::

## Error State

:::preview{title="Number input with an error message"}
<x-wirekit::number-input label="Age" name="age-error-demo" :value="0" error="Age must be between 1 and 120" />
:::

## With Hint

:::preview{title="Hint Text"}
<x-wirekit::number-input label="Guests" name="guests" :value="2" :min="1" :max="10" hint="Maximum 10 guests per reservation" />
:::

## Width

Like all WireKit form components, the number input fills its container (`w-full`). Control the width via the parent element — number inputs are typically narrow:

```blade
<div class="max-w-[10rem]">
    <x-wirekit::number-input label="Qty" name="qty" :value="1" :min="0" :max="99" />
</div>
```

See [Input — Width](/components/input#width) for more layout examples (grid columns, mixed widths).

## Livewire Integration

`<x-wirekit::number-input>` keeps its displayed value in internal state seeded
from the `value` prop, so when binding with Livewire pass the bound property as
`:value` **alongside** `wire:model` to seed the initial display:

```blade
<x-wirekit::number-input wire:model.live="budget" :value="$budget" min="0" />
```

Without `:value` the field shows `min` (or `0`) until the user interacts;
`wire:model` keeps it in sync afterward. This is the framework-agnostic seeding
pattern WireKit's stateful controls share — it works in plain Blade forms too.

## Optimistic UI

Pass the name of the Livewire method the field should call and the value is shown before the server answers — on a stepper click at once, and for the field when you leave it:

```blade
<x-wirekit::number-input
    name="quantity"
    label="Quantity"
    :value="$quantity"
    optimistic="saveQuantity"
/>
```

Load `wirekit-optimistic.js` alongside whichever bundle you already use — below it, in your layout:

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
```

### Try it

The demo below runs the real path: the change shows immediately, the outline says it is
provisional, and the server's answer either confirms it silently or takes it back.

:::preview{title="Optimistic number — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::number-input name="opt-accept" label="Accepted — the confirmation is silent" :value="3" optimistic="demoAccept" />
    <x-wirekit::number-input name="opt-reject" label="Refused — your number stays, and you are told it did not save" :value="3" optimistic="demoReject" />
    <x-wirekit::number-input name="opt-slow" label="Slow answer — the dashed outline is the provisional state" :value="3" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::number-input
    name="quantity"
    label="Quantity"
    :value="$quantity"
    optimistic="saveQuantity"
/>
:::

The `<livewire:demos.…>` wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.

**A refusal keeps the value — for the steppers too, and that is deliberate.** Stepping is a discrete choice, so putting it back would normally cost you nothing. But this control also has a field you can type in, and a number typed while a stepper's request is still out would be overwritten when that request rolls back. Rather than let safety depend on whether you happened to be typing, neither half undoes: the value stays and says it was not saved.

The field is **not** marked invalid. `aria-invalid` means *this value is wrong*, and a save that failed on the network says nothing about the value.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string\|null` | `null` | Label text above the input |
| `hint` | `string\|null` | `null` | Help text below the input |
| `error` | `string\|null` | `null` | Error message (also reads from `$errors`) |
| `name` | `string\|null` | `null` | Form field name |
| `id` | `string\|null` | auto-generated | Element id |
| `size` | `string` | `'md'` | `'sm'`, `'md'`, `'lg'` |
| `min` | `int\|float\|null` | `null` | Minimum allowed value |
| `max` | `int\|float\|null` | `null` | Maximum allowed value |
| `step` | `int\|float` | `1` | Increment/decrement step |
| `value` | `int\|float\|null` | `null` | Initial value |
| `prefix` | `string\|null` | `null` | Text/icon inside left |
| `suffix` | `string\|null` | `null` | Text/icon inside right |
| `disabled` | `bool` | `false` | Disabled state |
| `scope` | `string\|null` | `null` | Scoped personalization key |
| `optimistic` | `string\|null` | `null` | Livewire method to call when you leave the field or use the steppers, showing the new number as saving. A refusal **keeps your number** and says it was not saved. See [Optimistic UI](#optimistic-ui). |
| `optimisticArgs` | `array` | `[]` | Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |

## Accessibility

- Uses native `<input type="number">` — inherits browser validation and AT support
- Stepper buttons are `<button type="button">` with `aria-label="Increment"` / `aria-label="Decrement"` and `tabindex="-1"` (not in tab order — the input itself handles keyboard stepping)
- Native browser spinners are hidden via CSS to avoid double controls
- Arrow Up/Down keys increment/decrement by `step` (native behavior)
- `aria-invalid="true"` and `aria-describedby` set when `error` or `hint` are present
- `min`/`max` constraints enforced both by native validation and stepper button disabling
- Stepper icons are `aria-hidden="true"` — the button labels provide the accessible name

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus to the input |
| `ArrowUp` / `ArrowDown` | Increment / decrement by `step` |
| Numeric keys | Type the value directly |
| `Home` / `End` | Move caret to start / end |

## Pitfalls

- **Don't omit `min` / `max` for bounded ranges.** Without them, `step` arrows can produce invalid values that fail server-side validation — confusing for the user.
- **Don't use number-input for codes / IDs.** Phone numbers, OTPs, postal codes are STRINGS. Use `<x-wirekit::input>` with a `pattern` attribute to keep leading zeros and avoid scientific notation.

## Design Tokens

| Token | Purpose |
| --- | --- |
| `--color-wk-bg-input` | Input background |
| `--color-wk-border-strong` | Default border color |
| `--color-wk-border-error` | Border color on error |
| `--color-wk-text` | Input text color |
| `--color-wk-text-placeholder` | Placeholder text color |
| `--color-wk-text-muted` | Prefix/suffix text color |
| `--color-wk-accent` | Stepper button hover accent |
| `--color-wk-ring` | Focus ring color |
| `--size-wk-sm` / `md` / `lg` | Input height per size variant |
| `--radius-wk-md` | Border radius |
| `--transition-wk-duration` | Hover/focus transition speed |
| `--opacity-wk-disabled` | Dimmed state when disabled |

## Customization

Override defaults in `config/wirekit.php`:

```php
'components' => [
    'number-input' => ['size' => 'md', 'step' => 1],
],
```

## See Also

- [Inline Edit](/components/inline-edit) — edit this value in place, with an explicit confirm step

## Further Reading

- [MDN: `<input type="number">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number)
- [MDN: `aria-invalid`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid)
- [MDN: `aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)
- [WAI-ARIA Spinbutton Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/)
