A flexible and accessible numeric input stepper component for Vue 3 with long-press support and debounced saving
npm install vue-numeric-input-stepper


A flexible and accessible numeric input stepper component for Vue 3 with long-press support and debounced saving.
- ✅ Long-press support - Hold buttons to continuously increment/decrement
- ✅ Debounced saving - Automatic save with configurable delay
- ✅ Keyboard input - Direct numeric input with validation
- ✅ Accessibility - Full ARIA support and keyboard navigation
- ✅ TypeScript - Fully typed with TypeScript
- ✅ Vue 3 - Built with Composition API
- ✅ Customizable - Configurable step, intervals, and styling
``bash`
npm install vue-numeric-input-stepper
`vue
:min="1"
:max="100"
unit="pt"
@save="onSave"
@change="onChange"
/>
`
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | number | required | The current value (v-model) |min
| | number | 1 | Minimum value |max
| | number | 400 | Maximum value |step
| | number | 1 | Increment/decrement step |unit
| | string | '' | Unit label to display |showUnit
| | boolean | true | Show/hide unit label |disabled
| | boolean | false | Disable the component |interval
| | number | 100 | Long-press repeat interval (ms) |longPressDelay
| | number | 350 | Long-press detection delay (ms) |saveDelay
| | number | 800 | Debounce delay for save event (ms) |
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | number | Emitted when value changes (v-model) |save
| | number | Emitted after debounce delay when value is saved |change
| | { oldValue: number, newValue: number } | Emitted when value is committed |
| Slot | Description |
|------|-------------|
| decrement-icon | Custom icon for decrement button |increment-icon
| | Custom icon for increment button |
`vue`
−
+
`vue`
:min="0"
:max="1000"
:step="10"
unit="px"
/>
`vue`
:interval="50"
:long-press-delay="200"
:save-delay="500"
/>
`typescript
import { createApp } from 'vue';
import { install } from 'vue-numeric-input-stepper';
const app = createApp(App);
app.use({ install });
`
Then use it in any component:
`vue`
The component uses CSS variables for easy customization. You can override any style by setting CSS variables:
`css`
/ Example: Customize colors and sizes /
.numeric-input-stepper {
--stepper-button-width: 40px;
--stepper-button-height: 40px;
--stepper-button-bg: #007bff;
--stepper-button-color: white;
--stepper-button-hover-bg: #0056b3;
--stepper-input-width: 60px;
--stepper-input-font-size: 16px;
--stepper-gap: 12px;
}
| Variable | Default | Description |
|----------|---------|-------------|
| --stepper-gap | 8px | Gap between controls and unit |--stepper-padding
| | 5px 6px | Padding of the container |--stepper-controls-gap
| | 4px | Gap between buttons and input |--stepper-button-width
| | 32px | Button width |--stepper-button-height
| | 32px | Button height |--stepper-button-padding
| | 0 | Button padding |--stepper-button-border
| | 1px solid rgba(0, 0, 0, 0.2) | Button border |--stepper-button-bg
| | transparent | Button background |--stepper-button-color
| | currentColor | Button text/icon color |--stepper-button-hover-bg
| | rgba(0, 0, 0, 0.05) | Button hover background |--stepper-button-active-bg
| | rgba(0, 0, 0, 0.1) | Button active background |--stepper-button-disabled-opacity
| | 0.5 | Disabled button opacity |--stepper-button-radius
| | 4px | Button border radius |--stepper-icon-width
| | 16px | Icon width |--stepper-icon-height
| | 16px | Icon height |--stepper-icon-color
| | currentColor | Icon color |--stepper-icon-stroke
| | currentColor | Icon stroke color |--stepper-input-width
| | 44px | Input width |--stepper-input-height
| | 24px | Input height |--stepper-input-border
| | 1px solid rgba(0, 0, 0, 0.2) | Input border |--stepper-input-radius
| | 4px | Input border radius |--stepper-input-font-size
| | 14px | Input font size |--stepper-input-bg
| | transparent | Input background |--stepper-input-color
| | inherit | Input text color |--stepper-input-focus-border-color
| | rgba(0, 0, 0, 0.4) | Input focus border color |--stepper-input-disabled-opacity
| | 0.5 | Disabled input opacity |--stepper-unit-font-size
| | 13px | Unit label font size |--stepper-unit-color
| | rgba(0, 0, 0, 0.6) | Unit label color |
If you need to convert between display values (shown to users) and internal values (stored in database), you can use the value converter utility:
`typescript
import { createValueConverter, textSizeConverter } from 'vue-numeric-input-stepper';
// Use predefined text size converter (1-100 → 23-1000)
const { toInternal, toDisplay } = textSizeConverter;
const displayValue = 14; // What user sees
const internalValue = toInternal(displayValue); // 151 (what to save)
// Or create custom converter
const customConverter = createValueConverter({
displayMin: 1,
displayMax: 100,
internalMin: 0,
internalMax: 10000,
});
`
`vue
:min="1"
:max="100"
@save="onSave"
/>
`
Full TypeScript support is included:
`typescript
import type { ChangeEvent } from 'vue-numeric-input-stepper';
const handleChange = (event: ChangeEvent) => {
console.log(event.oldValue, event.newValue);
};
`
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Interactive documentation and examples are available in Storybook:
`bash`
npm run storybook
Then open http://localhost:6006` in your browser.
MIT
Contributions are welcome! Please feel free to submit a Pull Request.