How to build a slider in Vue

Range sliders are essential form controls for selecting numeric values within a bounded range, commonly used for filters, settings, and data visualization. With over 12 years of Vue.js development experience since 2014 and as the creator of CoreUI, I’ve built numerous custom slider components for production applications. A custom Vue slider requires reactive state for the current value, event handlers for user input, and computed properties for positioning the slider thumb. This approach creates a fully accessible slider without external dependencies.

Build a range slider using reactive state, v-model binding, and CSS for visual styling.

<template>
  <div class='slider-container'>
    <label v-if='label'>{{ label }}: {{ modelValue }}</label>
    <div class='slider-wrapper'>
      <input
        type='range'
        :min='min'
        :max='max'
        :step='step'
        :value='modelValue'
        @input='updateValue'
        class='slider'
      />
      <div class='slider-track'>
        <div class='slider-fill' :style='{ width: fillWidth }'></div>
      </div>
    </div>
    <div class='slider-labels'>
      <span>{{ min }}</span>
      <span>{{ max }}</span>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  modelValue: {
    type: Number,
    default: 50
  },
  min: {
    type: Number,
    default: 0
  },
  max: {
    type: Number,
    default: 100
  },
  step: {
    type: Number,
    default: 1
  },
  label: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['update:modelValue'])

const updateValue = (event) => {
  emit('update:modelValue', Number(event.target.value))
}

const fillWidth = computed(() => {
  const percentage = ((props.modelValue - props.min) / (props.max - props.min)) * 100
  return `${percentage}%`
})
</script>

<style scoped>
.slider-container {
  padding: 20px;
}

.slider-wrapper {
  position: relative;
  margin: 10px 0;
}

.slider {
  width: 100%;
  height: 6px;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  outline: none;
  position: relative;
  z-index: 2;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  background: #4CAF50;
  cursor: pointer;
  border-radius: 50%;
}

.slider::-moz-range-thumb {
  width: 20px;
  height: 20px;
  background: #4CAF50;
  cursor: pointer;
  border-radius: 50%;
  border: none;
}

.slider-track {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 6px;
  background: #ddd;
  transform: translateY(-50%);
  pointer-events: none;
  z-index: 1;
}

.slider-fill {
  height: 100%;
  background: #4CAF50;
  transition: width 0.1s;
}

.slider-labels {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: #666;
}
</style>

Use it with v-model:

<template>
  <Slider v-model='volume' :min='0' :max='100' label='Volume' />
</template>

<script setup>
import { ref } from 'vue'
import Slider from './Slider.vue'

const volume = ref(50)
</script>

Best Practice Note

The native <input type="range"> provides built-in accessibility with keyboard support (arrow keys). For more complex sliders with dual handles or vertical orientation, consider using a library like vue-slider-component. Add debouncing if the slider triggers expensive operations like API calls. This is similar to how we implement sliders in CoreUI for Vue—starting with native HTML elements for accessibility, then enhancing with custom styling and features for enterprise dashboards.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author