Skip to content

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 ​

js
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:

KeyAction
↑Increment by step
↓Decrement by step
Page UpIncrement by step × 10
Page DownDecrement by step × 10
HomeJump to minimum value
EndJump to maximum value
Mouse WheelIncrement/decrement (when allowMouseWheel: true)

API Reference ​

Component Props ​

PropTypeDefaultDescription
valuenumber0Initial value
minnumber-Minimum allowed value
maxnumber-Maximum allowed value
stepnumber1Increment/decrement step
disabledbooleanfalseDisable the input
readonlybooleanfalseMake input read-only
allowMouseWheelbooleanfalseAllow mouse wheel to change value
clampValueOnBlurbooleantrueClamp value to min/max on blur
formatOptionsIntl.NumberFormatOptions-Number formatting options
translationsobject-Custom labels (incrementLabel, decrementLabel)
html
<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.

html
<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 ​

PartDescription
x-number-inputRoot container element
x-number-input:rootAlternative root container
x-number-input:labelLabel element for the input field
x-number-input:inputThe actual input field
x-number-input:incrementButton to increment the value
x-number-input:decrementButton to decrement the value
x-number-input:scrubberOptional drag element to change value

x-number-input:root ​

Container for the entire number input component.

Automatically receives:

  • data-disabled - Present when disabled
  • data-readonly - Present when read-only
  • data-focused - Present when input is focused
html
<div x-number-input:root>
  <!-- All parts -->
</div>

x-number-input:label ​

Label element for the input field.

Automatically receives:

  • Proper for attribute linking to input
html
<label x-number-input:label>Quantity</label>

x-number-input:input ​

The actual input field.

Automatically receives:

  • type="text" with inputmode="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
html
<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-disabled attribute when disabled
  • Click handler to increment and refocus input
html
<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-disabled attribute when disabled
  • Click handler to decrement and refocus input
html
<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: none and user-select: none
  • data-disabled when component is disabled or read-only

Behavior:

  • Drag up: increases value
  • Drag down: decreases value
  • 10 pixels of drag = 1 step
html
<div x-number-input:scrubber title="Drag to change value">
  ⇅
</div>

Data Attributes ​

AttributeDescription
data-disabledPresent when disabled on root, increment, decrement, scrubber
data-readonlyPresent when read-only on root
data-focusedPresent when input is focused on root
html
<div x-number-input:root class="data-disabled:opacity-50 data-readonly:cursor-not-allowed data-focused:ring-2">
  <!-- Parts -->
</div>

Events ​

EventDetailDescription
change{ value: number }Fired when value changes
focus-Fired when input gains focus
blur-Fired when input loses focus
html
<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:

html
<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 value
  • inputValue (string) - Formatted display value
  • focused (boolean) - Whether input is focused
  • isAtMin (boolean) - Whether value is at minimum
  • isAtMax (boolean) - Whether value is at maximum
  • canIncrement (boolean) - Whether increment is allowed
  • canDecrement (boolean) - Whether decrement is allowed

Available methods:

  • setValue(newValue, dispatch?) - Set value programmatically
  • increment(multiplier?) - Increment by step × multiplier
  • decrement(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 ​

html
<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>

See Also ​

Built with Alpine.js and inspired by Zag.js