TouchSpin Vue

October 24, 2025 · View on GitHub

Vue adapter for TouchSpin numeric input spinner - A feature-rich, accessible number input component with increment/decrement buttons.

Features

  • Vue 3 Composition API - Modern Vue 3 with <script setup> support
  • Touch & Mouse Support - Works on desktop and mobile devices
  • Accessible - Full keyboard navigation and screen reader support
  • TypeScript - Complete type definitions included
  • Performance - Lightweight and fast
  • Customizable - Extensive styling and behavior options
  • Reactive & Imperative - Both declarative props and direct API control

Installation

npm install @touchspin/vue @touchspin/core
# or
yarn add @touchspin/vue @touchspin/core
# or
pnpm add @touchspin/vue @touchspin/core

Quick Start

<template>
  <TouchSpin
    v-model="value"
    :min="0"
    :max="100"
    :step="1"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'
import '@touchspin/renderer-vanilla/css'

const value = ref(25)
</script>

Available Renderers

Choose the renderer that matches your design system:

RendererImportCSS ImportDescription
Vanilla@touchspin/vue/vanilla@touchspin/renderer-vanilla/cssClean, framework-free styling

API Reference

Props

Value Management

<TouchSpin
  v-model="number"                   <!-- Current value -->
  :default-value="number"            <!-- Initial value (uncontrolled) -->
  @change="(value, meta) => void"    <!-- Value change handler -->
/>

Configuration

<TouchSpin
  :min="number"                      <!-- Minimum value -->
  :max="number"                      <!-- Maximum value -->
  :step="number"                     <!-- Increment/decrement step -->
  :decimals="number"                 <!-- Decimal places -->
  :prefix="'string'"                 <!-- Text before input -->
  :suffix="'string'"                 <!-- Text after input -->
/>

State & Behavior

<TouchSpin
  :disabled="boolean"                <!-- Disable input and buttons -->
  :read-only="boolean"               <!-- Make input read-only -->
/>

Form Integration

<TouchSpin
  name="string"                      <!-- Form field name -->
  id="string"                        <!-- Input element ID -->
/>

Styling

<TouchSpin
  class="string"                     <!-- Wrapper CSS class -->
  :input-class="'string'"             <!-- Input CSS class -->
/>

Events

<TouchSpin
  @blur="FocusEventHandler"          <!-- Input blur event -->
  @focus="FocusEventHandler"         <!-- Input focus event -->

  <!-- TouchSpin Events -->
  @on-min="() => void"                <!-- Fired at minimum boundary -->
  @on-max="() => void"                <!-- Fired at maximum boundary -->
  @on-start-spin="() => void"         <!-- Fired when spinning starts -->
  @on-stop-spin="() => void"          <!-- Fired when spinning stops -->
  @on-start-up-spin="() => void"      <!-- Fired when upward spinning starts -->
  @on-start-down-spin="() => void"    <!-- Fired when downward spinning starts -->
  @on-stop-up-spin="() => void"       <!-- Fired when upward spinning stops -->
  @on-stop-down-spin="() => void"     <!-- Fired when downward spinning stops -->
  @on-speed-change="() => void"       <!-- Fired when spin speed increases -->
/>

Advanced

<TouchSpin
  :core-options="object"             <!-- Advanced TouchSpin core options -->
  :input-props="object"              <!-- Pass-through props for input -->
  data-testid="string"               <!-- Test ID (input gets -input suffix) -->
/>

Imperative API (ref)

For direct programmatic control, use a template ref:

<template>
  <div>
    <button @click="handleIncrement">+1</button>
    <TouchSpin ref="touchSpinRef" v-model="value" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'
import type { TouchSpinHandle } from '@touchspin/vue/vanilla'

const touchSpinRef = ref<TouchSpinHandle>()
const value = ref(50)

const handleIncrement = () => {
  touchSpinRef.value?.increment()
}
</script>

TouchSpinHandle Methods

interface TouchSpinHandle {
  // Focus Management
  focus(): void;                    // Focus the input
  blur(): void;                     // Blur the input

  // Value Control
  increment(): void;                // Increment by step
  decrement(): void;                // Decrement by step
  getValue(): number;               // Get current value
  setValue(value: number): void;    // Set new value

  // Continuous Spinning
  startUpSpin(): void;              // Start continuous upward spinning
  startDownSpin(): void;            // Start continuous downward spinning
  stopSpin(): void;                 // Stop any continuous spinning

  // Configuration
  updateSettings(opts: Partial<TouchSpinCoreOptions>): void;
                                  // Update settings at runtime
}

Usage Examples

Basic Controlled Component

<template>
  <div>
    <TouchSpin
      v-model="value"
      :min="0"
      :max="100"
      :step="5"
    />
    <p>Value: {{ value }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'

const value = ref(10)
</script>

With Prefix/Suffix

<template>
  <TouchSpin
    v-model="price"
    :min="0"
    :max="1000"
    :step="0.01"
    :decimals="2"
    prefix="$"
    suffix=" USD"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'

const price = ref(29.99)
</script>

Event Handling

<template>
  <div>
    <TouchSpin
      v-model="value"
      :min="0"
      :max="100"
      @on-min="addEvent('Reached minimum')"
      @on-max="addEvent('Reached maximum')"
      @on-start-spin="addEvent('Spin started')"
      @on-stop-spin="addEvent('Spin stopped')"
    />

    <h3>Event Log:</h3>
    <ul>
      <li v-for="(event, i) in events" :key="i">{{ event }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'

const value = ref(50)
const events = ref<string[]>([])

const addEvent = (eventName: string) => {
  events.value.push(`${new Date().toLocaleTimeString()}: ${eventName}`)
}
</script>

Imperative Control

<template>
  <div>
    <div>
      <button @click="setValue42">Set to 42</button>
      <button @click="startSpinning">Start Spinning Up</button>
      <button @click="stopSpinning">Stop Spinning</button>
      <button @click="updateValue">Get Current Value</button>
    </div>

    <TouchSpin
      ref="touchSpinRef"
      v-model="value"
      :min="0"
      :max="100"
    />

    <p>Current value: {{ currentValue }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'
import type { TouchSpinHandle } from '@touchspin/vue/vanilla'

const touchSpinRef = ref<TouchSpinHandle>()
const value = ref(25)
const currentValue = ref(0)

const setValue42 = () => {
  touchSpinRef.value?.setValue(42)
}

const startSpinning = () => {
  touchSpinRef.value?.startUpSpin()
}

const stopSpinning = () => {
  touchSpinRef.value?.stopSpin()
}

const updateValue = () => {
  currentValue.value = touchSpinRef.value?.getValue() ?? 0
}
</script>

Form Integration

<template>
  <form @submit.prevent="handleSubmit">
    <label>
      Quantity:
      <TouchSpin
        v-model="quantity"
        name="quantity"
        :min="1"
        :max="99"
      />
    </label>
    <button type="submit">Add to Cart</button>
  </form>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'

const quantity = ref(1)

const handleSubmit = () => {
  console.log('Quantity:', quantity.value)
}
</script>

Advanced Configuration

Custom Core Options

<template>
  <TouchSpin
    v-model="value"
    :min="0"
    :max="100"
    :step="1"
    :core-options="{
      verticalbuttons: true,
      buttonup_class: 'custom-up',
      buttondown_class: 'custom-down'
    }"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TouchSpin from '@touchspin/vue/vanilla'

const value = ref(50)
</script>

Testing

import { mount } from '@vue/test-utils'
import TouchSpin from '@touchspin/vue/vanilla'

test('increments value', async () => {
  const wrapper = mount(TouchSpin, {
    props: {
      modelValue: 5,
      min: 0,
      max: 10
    }
  })

  const incrementBtn = wrapper.find('[data-touchspin-injected="up"]')
  await incrementBtn.trigger('click')

  expect(wrapper.vm.modelValue).toBe(6)
})

Development

# Install dependencies
yarn install

# Start development server
yarn dev

# Build all packages
yarn build

# Run tests
yarn test

Core

  • @touchspin/core - Core TouchSpin logic and API

Renderers

  • @touchspin/renderer-vanilla - Vanilla CSS renderer

Adapters

  • @touchspin/angular - Angular adapter
  • @touchspin/react - React adapter
  • @touchspin/vue - Vue adapter (this package)
  • @touchspin/jquery - jQuery plugin
  • @touchspin/webcomponent - Web Components
  • @touchspin/standalone - Standalone bundle

Contributing

Contributions welcome! Please see the main TouchSpin repository for contribution guidelines.

License

MIT © Istvan Ujj-Meszaros