Number Input
A fully accessible number input component with increment/decrement controls, keyboard navigation, mouse wheel support, and optional scrubbing functionality.
Features
- ✅ Min/max value constraints
- ✅ Custom step increments
- ✅ Keyboard navigation (Arrow keys, Page Up/Down, Home/End)
- ✅ Mouse wheel support (optional)
- ✅ Virtual scrubbing (drag to change value)
- ✅ Number formatting (currency, percentage, etc.)
- ✅ WAI-ARIA spinbutton pattern
- ✅ Floating point precision handling
Installation
import Alpine from 'alpinejs'
import numberInput from 'alpine-headless-ui/number-input'
Alpine.plugin(numberInput)
Alpine.start()Examples
Basic Number Input
A simple number input with increment and decrement buttons.
With Min/Max Constraints
Number input with minimum and maximum value limits.
Range: 0-100, Step: 5
Currency Format
Number input with currency formatting.
Decimal Steps
Number input with decimal step values.
With Mouse Wheel
Number input that allows changing value using mouse wheel when focused.
Focus the input and use your mouse wheel to change the value
With Scrubber
Number input with virtual scrubbing (drag up/down to change value).
Click and drag the ⇅ icon to scrub the value
Disabled State
With x-model
Number input supports x-model for two-way binding.
Current value:
With Change Event
Current value:
Keyboard Shortcuts
The number input supports comprehensive keyboard navigation when focused:
| Key | Action |
|---|---|
| ↑ | Increment by step |
| ↓ | Decrement by step |
| Page Up | Increment by step × 10 |
| Page Down | Decrement by step × 10 |
| Home | Jump to minimum value |
| End | Jump to maximum value |
| Mouse Wheel | Increment/decrement (when allowMouseWheel: true) |
API Reference
Component Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Initial value |
min | number | - | Minimum allowed value |
max | number | - | Maximum allowed value |
step | number | 1 | Increment/decrement step |
disabled | boolean | false | Disable the input |
readonly | boolean | false | Make input read-only |
allowMouseWheel | boolean | false | Allow mouse wheel to change value |
clampValueOnBlur | boolean | true | Clamp value to min/max on blur |
formatOptions | Intl.NumberFormatOptions | - | Number formatting options |
translations | object | - | Custom labels (incrementLabel, decrementLabel) |
<div x-number-input="{
value: 10,
min: 0,
max: 100,
step: 5,
allowMouseWheel: true,
formatOptions: { style: 'currency', currency: 'USD' }
}">
<!-- Parts -->
</div>
<!-- With x-model for two-way binding -->
<div x-data="{ quantity: 10 }">
<div x-number-input="{ min: 0, max: 100 }" x-model="quantity">
<!-- Parts -->
</div>
</div>Two-way binding with x-model:
The number input component is modelable, which means you can use Alpine's x-model directive to create a two-way binding with your data. When you use x-model, the value prop is optional as the model will control the value.
<div x-data="{ count: 5 }">
<div x-number-input="{ min: 0, max: 10 }" x-model="count">
<!-- Parts -->
</div>
<p>Count: <span x-text="count"></span></p>
</div>Parts
| Part | Description |
|---|---|
x-number-input | Root container element |
x-number-input:root | Alternative root container |
x-number-input:label | Label element for the input field |
x-number-input:input | The actual input field |
x-number-input:increment | Button to increment the value |
x-number-input:decrement | Button to decrement the value |
x-number-input:scrubber | Optional drag element to change value |
x-number-input:root
Container for the entire number input component.
Automatically receives:
data-disabled- Present when disableddata-readonly- Present when read-onlydata-focused- Present when input is focused
<div x-number-input:root>
<!-- All parts -->
</div>x-number-input:label
Label element for the input field.
Automatically receives:
- Proper
forattribute linking to input
<label x-number-input:label>Quantity</label>x-number-input:input
The actual input field.
Automatically receives:
type="text"withinputmode="decimal"role="spinbutton"- ARIA attributes (
aria-valuemin,aria-valuemax,aria-valuenow) - Two-way binding to value
- All keyboard event handlers
- Mouse wheel handler (if enabled)
- Focus/blur handlers
<input x-number-input:input class="..." />x-number-input:increment
Button to increment the value.
Automatically receives:
type="button"tabindex="-1"(prevents tab focus)- ARIA label
- Disabled state when at max or component disabled
data-disabledattribute when disabled- Click handler to increment and refocus input
<button x-number-input:increment>+</button>x-number-input:decrement
Button to decrement the value.
Automatically receives:
type="button"tabindex="-1"(prevents tab focus)- ARIA label
- Disabled state when at min or component disabled
data-disabledattribute when disabled- Click handler to decrement and refocus input
<button x-number-input:decrement>−</button>x-number-input:scrubber
Optional element that allows dragging up/down to change the value.
Automatically receives:
- Pointer event handlers for drag functionality
- Cursor style (
ns-resize) touch-action: noneanduser-select: nonedata-disabledwhen component is disabled or read-only
Behavior:
- Drag up: increases value
- Drag down: decreases value
- 10 pixels of drag = 1 step
<div x-number-input:scrubber title="Drag to change value">
⇅
</div>Data Attributes
| Attribute | Description |
|---|---|
data-disabled | Present when disabled on root, increment, decrement, scrubber |
data-readonly | Present when read-only on root |
data-focused | Present when input is focused on root |
<div x-number-input:root class="data-disabled:opacity-50 data-readonly:cursor-not-allowed data-focused:ring-2">
<!-- Parts -->
</div>Events
| Event | Detail | Description |
|---|---|---|
change | { value: number } | Fired when value changes |
focus | - | Fired when input gains focus |
blur | - | Fired when input loses focus |
<div x-number-input
x-on:change="console.log('Value:', $event.detail.value)"
x-on:focus="console.log('Focused')"
x-on:blur="console.log('Blurred')">
<!-- Parts -->
</div>Accessing State
You can access the number input API using $numberInput:
<div x-number-input="{ value: 10, min: 0, max: 100 }">
<div x-data>
<p x-show="$numberInput.isAtMin">At minimum!</p>
<p x-show="$numberInput.isAtMax">At maximum!</p>
<p x-text="'Current value: ' + $numberInput.value"></p>
</div>
<input x-number-input:input />
</div>Available properties:
value(number) - Current numeric valueinputValue(string) - Formatted display valuefocused(boolean) - Whether input is focusedisAtMin(boolean) - Whether value is at minimumisAtMax(boolean) - Whether value is at maximumcanIncrement(boolean) - Whether increment is allowedcanDecrement(boolean) - Whether decrement is allowed
Available methods:
setValue(newValue, dispatch?)- Set value programmaticallyincrement(multiplier?)- Increment by step × multiplierdecrement(multiplier?)- Decrement by step × multiplier
Accessibility
This component implements the WAI-ARIA Spinbutton pattern.
Features
- ✅ Proper
role="spinbutton"on input - ✅ ARIA attributes:
aria-valuemin,aria-valuemax,aria-valuenow - ✅ ARIA labels on increment/decrement buttons
- ✅ Keyboard navigation with Arrow keys, Page Up/Down, Home/End
- ✅ Increment/decrement buttons have
tabindex="-1"(keyboard users use arrow keys) - ✅ Proper disabled and readonly states
- ✅ Input refocuses after button clicks
Best Practices
- Always provide a label using
x-number-input:label - Ensure sufficient color contrast for all states
- Test keyboard navigation thoroughly
- Consider providing visual feedback for min/max boundaries
- For currency inputs, announce the currency in the label
Example with Full Accessibility
<div x-number-input="{
value: 50,
min: 0,
max: 100,
translations: {
incrementLabel: 'Increase quantity',
decrementLabel: 'Decrease quantity'
}
}">
<label x-number-input:label>
Product Quantity
</label>
<div class="flex gap-2">
<button
x-number-input:decrement
class="... data-disabled:cursor-not-allowed"
>
−
</button>
<input
x-number-input:input
class="..."
aria-describedby="quantity-hint"
/>
<button
x-number-input:increment
class="... data-disabled:cursor-not-allowed"
>
+
</button>
</div>
<p id="quantity-hint" class="text-sm text-gray-500">
Use arrow keys to adjust quantity
</p>
</div>