Number Input
The <x-wirekit:: 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
With Step
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.
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_ 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; typing19,99is rejected. - A German / French / Italian locale user typing accepts BOTH
19.99AND19,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_ 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
Size Variants
Error State
Age must be between 1 and 120
With 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:
<div class="max-w-[10rem]">
<x-wirekit::number-input label="Qty" name="qty" :value="1" :min="0" :max="99" />
</div>
See Input — Width for more layout examples (grid columns, mixed widths).
Livewire Integration
Bind the field with wire:model and it shows the bound value from the first render, follows a value your component sets on the server while the field stays on the page, and sends both typing and stepper clicks to the property:
<x-wirekit::number-input label="Budget" wire:model.live="budget" min="0" />
With a model on the field, the model owns the input and the steppers follow it. The model stays on the input itself, and bound to a property declared int or float it reads the field as a number: wire:model.live goes out as wire:model., so the typed number the server sends back is not taken for a change that overwrites what the reader entered while the request was out. A property without a type, or a string one, gets the text, and the other modifiers behave as they do on a plain <input type="number">. An Alpine x-model works the same way, and without any model the value prop seeds the field.
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:
<x-wirekit::number-input
name="quantity"
label="Quantity"
:value="$quantity"
optimistic="saveQuantity"
/>
Load wirekit-optimistic. alongside whichever bundle you already use — below it, in your layout:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></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.
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 |
hideLabel |
bool |
false |
Render the label visually hidden (sr-only) but keep it for assistive tech — for a compact stepper in a toolbar or a table cell |
hint |
string|null |
null |
Help text below the input |
help |
string|null |
null |
An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label |
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 |
Disables the field and its − and + buttons. A readonly field keeps its buttons out of use as well |
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. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
announceError |
bool|null | null |
Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region. |
Slots
| Slot | Description |
|---|---|
labelAfter |
Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label. |
Accessibility
- Uses native
<input type="number">— inherits browser validation and AT support - Stepper buttons are
<button type="button">withtabindex="-1"andaria-controlspointing at the field — not in the tab order, becauseArrowUp/ArrowDownon the input step to the same grid points, and the native spinner these buttons replace is not a tab stop either - Their accessible name carries the field's label when you pass one —
Decrease Quantity/Increase Quantity— so a page with several number fields does not expose a row of identically named controls to a screen reader reading it in browse mode. Without alabelthey take the field'saria-label, and without either they readDecrease/Increase - Native browser spinners are hidden via CSS to avoid double controls
- Arrow Up/Down keys increment/decrement by
step(native behavior) aria-invalid="true"andaria-describedbyset whenerrororhintare presentminandmaxhold when the field is left: a typed value outside them is set to the nearest bound, an emptied field tomin(or 0), and a value finer thanstepis rounded to it. The field then says what it did, under itself and to a screen reader, "Set to 99, the highest value allowed.", until the next keystroke. The steppers stop at the bounds- 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/maxfor bounded ranges. Without them,steparrows 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::with ainput> patternattribute 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-bg-active |
Stepper button background on hover |
--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/:
'components' => [
'number-input' => ['size' => 'md', 'step' => 1],
],
See Also
- Inline Edit — edit this value in place, with an explicit confirm step