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