Components
Number Input
A numeric stepper with minus/plus buttons that clamp a value to an optional min, max and step.
Building a cart or product quantity stepper? Use number-input with :min="1" and a compact size="sm" — see the Quantity selector example below. (A separate quantity-selector component was removed in favour of this; it was the same control with different defaults.)
<x-ui.number-input name="quantity" :value="1" class="w-40" />
Currency Mask
{{-- Cents-style entry for a till or a price form: digits enter from the right, so typing
1 9 0 shows 1.90 and Backspace drops the last digit. Pasting "1.9" keeps it 1.90. --}}
<div class="grid w-full max-w-xs items-center gap-2">
<x-ui.label for="amount">Amount</x-ui.label>
<x-ui.number-input id="amount" name="amount" :value="null" :min="0" :step="0.01" :decimals="2" mask class="w-40" />
</div>
Disabled
<x-ui.number-input name="quantity" :value="3" :disabled="true" class="w-40" />
Linked Fields
{{-- Two fields derived from each other: a price without and with an 18% tax. Each one
derives the OTHER from number-change, which only the user's own edits fire, so a value
written into a field never comes back and nothing needs a skip flag. Under Livewire bind
each to its property with wire:model and the same handlers write through to it. --}}
<div
x-data="{
rate: 0.18,
derive(id, v, factor) {
Alpine.$data(document.getElementById(id)).value = v === null ? null : this.$blatNumber.round(v * factor, 2);
},
}"
class="grid w-full max-w-md gap-4 sm:grid-cols-2"
>
<div class="grid gap-2">
<x-ui.label for="price-net">Price (excl. tax)</x-ui.label>
<x-ui.number-input
id="price-net"
name="price_net"
:value="10"
:min="0"
:step="0.01"
:decimals="2"
mask
x-on:number-change="derive('price-gross', $event.detail.value, 1 + rate)"
/>
</div>
<div class="grid gap-2">
<x-ui.label for="price-gross">Price (incl. tax)</x-ui.label>
<x-ui.number-input
id="price-gross"
name="price_gross"
:value="11.8"
:min="0"
:step="0.01"
:decimals="2"
mask
x-on:number-change="derive('price-net', $event.detail.value, 1 / (1 + rate))"
/>
</div>
</div>
Min Max
<x-ui.number-input name="guests" :value="2" :min="1" :max="10" class="w-40" />
Quantity Selector
{{-- A cart / product quantity stepper: number-input with a floor of 1 and a
compact footprint. Replaces the former standalone quantity-selector. --}}
<x-ui.number-input name="quantity" :value="1" :min="1" :max="10" step="1" size="sm" class="w-32" />
Sizes
<div class="flex flex-col items-start gap-4">
<x-ui.number-input name="qty-sm" size="sm" :value="1" class="w-36" />
<x-ui.number-input name="qty-default" size="default" :value="1" class="w-40" />
<x-ui.number-input name="qty-lg" size="lg" :value="1" class="w-44" />
</div>
Step
<x-ui.number-input name="rate" :value="2.5" :step="0.5" :min="0" class="w-40" />
With Decimals
<x-ui.number-input name="price" :value="1.9" :step="0.1" :min="0" :decimals="2" :nullable="false" class="w-40" />
Using with Livewire
wire:model
The examples above are the frontend (Blade + Alpine) usage. Inside a Livewire component, bind
wire:model for two-way
state — same component, no wrappers.
Full Livewire guide →
use Livewire\Component;
class Demo extends Component
{
public int $qty = 1;
public function render()
{
return view('livewire.demo');
}
}
<x-ui.number-input wire:model="qty" :min="1" />
API Reference
Props, slots and exposed methods for
<x-ui.number-input>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string
|
— | Name of the underlying input, used for form submission and as the fallback accessible label when nothing else names the field. |
value |
number
|
0
|
Initial numeric value. Pass null or an empty string to start the field empty. |
min |
number
|
— | Lowest allowed value. Clamps on blur and disables the decrease button at the floor. |
max |
number
|
— | Highest allowed value. Clamps on blur and disables the increase button at the ceiling. |
step |
number
|
1
|
Amount each increase/decrease button adds or subtracts. |
decimals |
int
|
— | Fixed decimal places. The field shows the value at this precision (1.90, not 1.9) and rounds what it sends. The bound value stays a number, so trailing zeroes are presentation only. |
nullable |
bool
|
true
|
Whether an emptied field is committed as null on blur. Pass false for a field backed by a non-nullable property: the field then reverts to its previous value. Either way nothing is sent while the field is empty mid-edit. |
size |
string
|
'default'
|
Control height and text size; matches the input component so fields line up in a form.
sm
default
lg
|
disabled |
bool
|
false
|
Disables the field and both stepper buttons. |
id |
string
|
— | id attribute of the underlying input, for associating an external label. With an id, a `<label for>` names the field; the fallback label (name, then "Number") is only applied when no label points at it. |
placeholder |
string
|
— | Placeholder shown while the field is empty. |
mask |
bool
|
false
|
Cents-style currency mask: digits enter from the right at `decimals` places (2 when unset), so typing 190 shows 1.90 and Backspace drops the last digit. Pasted text with a dot or comma is read as a normal number (1.9 stays 1.90). The bound value is written on every keystroke, client side, so a `$watch` on a plain `wire:model` sees each digit. A digit that would pass `max` is refused; `min`, `nullable` and the stepper buttons behave as without the mask. |
Events
Events the component listens for (in) and dispatches (out). Both directions bubble,
so .window works
even when the component is teleported.
| Event | Direction | Detail |
|---|---|---|
number-change |
Out |
{ value }
The user changed the value: a keystroke that produced a number, a paste, a stepper click, or the commit on blur (value may then be null). A value written from outside, by the server or by another field, never fires it. That is what makes two fields derived from each other (a price with and without tax) safe: derive the other field in this handler and it cannot echo back.
|