Range Sliders
MediumRange input sliders with multiple handles and value displays
Updated about 2 months ago
Tested & Responsive
Live Demo
Preview:
Desktop view
Basic Slider
Slider with Min/Max Labels
$0
$1000
Gradient Slider
10°C
Cold
Comfortable
Hot
30°C
Stepped Slider with Indicators
Poor
Fair
Good
Great
Excellent
Resize to see how the component adapts to different screen sizes
<div class="space-y-8">
<!-- Basic Slider -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Basic Slider</h3>
<div class="max-w-md mx-auto">
<div data-controller="slider" data-slider-min-value="0" data-slider-max-value="100" data-slider-value="50" data-slider-show-value-value="true">
<label class="block text-sm font-medium text-green-900 mb-4">
Volume: <span data-slider-target="value" class="font-semibold text-green-600">50</span>%
</label>
<input
data-slider-target="input"
data-action="input->slider#updateValue"
type="range"
min="0"
max="100"
value="50"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb:appearance-none slider-thumb:h-4 slider-thumb:w-4 slider-thumb:rounded-full slider-thumb:bg-green-600 slider-thumb:cursor-pointer"
>
</div>
</div>
</div>
<!-- Slider with Min/Max Labels -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Slider with Min/Max Labels</h3>
<div class="max-w-md mx-auto">
<div data-controller="slider" data-slider-min-value="0" data-slider-max-value="1000" data-slider-value="250" data-slider-step="50" data-slider-show-value-value="true" data-slider-show-min-max-value="true">
<label class="block text-sm font-medium text-green-900 mb-2">
Budget: $<span data-slider-target="value" class="font-semibold text-green-600">250</span>
</label>
<div class="relative">
<input
data-slider-target="input"
data-action="input->slider#updateValue"
type="range"
min="0"
max="1000"
step="50"
value="250"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb:appearance-none slider-thumb:h-4 slider-thumb:w-4 slider-thumb:rounded-full slider-thumb:bg-green-600 slider-thumb:cursor-pointer"
>
</div>
<div class="flex justify-between text-sm text-gray-500 mt-2">
<span>$<span data-slider-target="min">0</span></span>
<span>$<span data-slider-target="max">1000</span></span>
</div>
</div>
</div>
</div>
<!-- Gradient Slider -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Gradient Slider</h3>
<div class="max-w-md mx-auto">
<div data-controller="slider" data-slider-min-value="10" data-slider-max-value="30" data-slider-value="22" data-slider-step="0.5" data-slider-show-value-value="true">
<label class="block text-sm font-medium text-green-900 mb-4">
Temperature: <span data-slider-target="value" class="font-semibold text-green-600">22</span>°C
</label>
<div class="relative">
<input
data-slider-target="input"
data-action="input->slider#updateValue"
type="range"
min="10"
max="30"
step="0.5"
value="22"
class="w-full h-2 bg-gradient-to-r from-blue-300 via-green-300 to-red-300 rounded-lg appearance-none cursor-pointer slider-thumb:appearance-none slider-thumb:h-5 slider-thumb:w-5 slider-thumb:rounded-full slider-thumb:bg-white slider-thumb:border-2 slider-thumb:border-gray-400 slider-thumb:cursor-pointer slider-thumb:shadow-md"
>
</div>
<div class="flex justify-between text-sm text-gray-500 mt-2">
<span>10°C</span>
<span class="text-blue-600">Cold</span>
<span class="text-green-600">Comfortable</span>
<span class="text-red-600">Hot</span>
<span>30°C</span>
</div>
</div>
</div>
</div>
<!-- Stepped Slider with Indicators -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Stepped Slider with Indicators</h3>
<div class="max-w-md mx-auto">
<div data-controller="slider" data-slider-min-value="1" data-slider-max-value="5" data-slider-value="3" data-slider-step="1" data-slider-show-value-value="true">
<label class="block text-sm font-medium text-green-900 mb-4">
Rating: <span data-slider-target="value" class="font-semibold text-green-600">3</span> stars
</label>
<div class="relative">
<input
data-slider-target="input"
data-action="input->slider#updateValue"
type="range"
min="1"
max="5"
step="1"
value="3"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb:appearance-none slider-thumb:h-4 slider-thumb:w-4 slider-thumb:rounded-full slider-thumb:bg-amber-500 slider-thumb:cursor-pointer"
>
<!-- Step indicators -->
<div class="flex justify-between mt-2">
<div class="w-2 h-2 bg-gray-300 rounded-full"></div>
<div class="w-2 h-2 bg-gray-300 rounded-full"></div>
<div class="w-2 h-2 bg-gray-300 rounded-full"></div>
<div class="w-2 h-2 bg-gray-300 rounded-full"></div>
<div class="w-2 h-2 bg-gray-300 rounded-full"></div>
</div>
</div>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>Poor</span>
<span>Fair</span>
<span>Good</span>
<span>Great</span>
<span>Excellent</span>
</div>
</div>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="slider"
export default class extends Controller {
static targets = ["input", "track", "thumb", "value", "min", "max", "fill"]
static values = {
min: Number,
max: Number,
step: Number,
value: Number,
showValue: Boolean,
showMinMax: Boolean,
vertical: Boolean
}
connect() {
this.setupSlider()
this.updateDisplay()
}
setupSlider() {
// Set initial values
if (this.hasInputTarget) {
this.inputTarget.min = this.minValue || 0
this.inputTarget.max = this.maxValue || 100
this.inputTarget.step = this.stepValue || 1
this.inputTarget.value = this.valueValue || this.minValue || 0
}
// Update min/max displays
if (this.showMinMaxValue) {
if (this.hasMinTarget) {
this.minTarget.textContent = this.minValue || 0
}
if (this.hasMaxTarget) {
this.maxTarget.textContent = this.maxValue || 100
}
}
}
updateValue(event) {
const value = parseFloat(event.target.value)
this.updateDisplay(value)
this.dispatchChangeEvent(value)
}
updateDisplay(value = null) {
const currentValue = value !== null ? value : parseFloat(this.inputTarget.value)
const min = this.minValue || 0
const max = this.maxValue || 100
const percentage = ((currentValue - min) / (max - min)) * 100
// Update value display
if (this.showValueValue && this.hasValueTarget) {
this.valueTarget.textContent = this.formatValue(currentValue)
}
// Update fill/progress bar
if (this.hasFillTarget) {
if (this.verticalValue) {
this.fillTarget.style.height = `${percentage}%`
} else {
this.fillTarget.style.width = `${percentage}%`
}
}
// Update thumb position for custom sliders
if (this.hasThumbTarget) {
if (this.verticalValue) {
this.thumbTarget.style.bottom = `${percentage}%`
} else {
this.thumbTarget.style.left = `${percentage}%`
}
}
// Update data attribute for CSS styling
this.element.dataset.value = currentValue
this.element.dataset.percentage = Math.round(percentage)
}
formatValue(value) {
// Format the value for display (can be customized)
if (this.stepValue && this.stepValue < 1) {
return value.toFixed(1)
}
return Math.round(value).toString()
}
setValue(value) {
const clampedValue = Math.max(this.minValue || 0, Math.min(this.maxValue || 100, value))
this.inputTarget.value = clampedValue
this.updateDisplay(clampedValue)
this.dispatchChangeEvent(clampedValue)
}
getValue() {
return parseFloat(this.inputTarget.value)
}
increment() {
const step = this.stepValue || 1
const newValue = this.getValue() + step
this.setValue(newValue)
}
decrement() {
const step = this.stepValue || 1
const newValue = this.getValue() - step
this.setValue(newValue)
}
// Handle keyboard events for custom sliders
handleKeydown(event) {
const step = this.stepValue || 1
const currentValue = this.getValue()
switch (event.key) {
case 'ArrowRight':
case 'ArrowUp':
event.preventDefault()
this.setValue(currentValue + step)
break
case 'ArrowLeft':
case 'ArrowDown':
event.preventDefault()
this.setValue(currentValue - step)
break
case 'Home':
event.preventDefault()
this.setValue(this.minValue || 0)
break
case 'End':
event.preventDefault()
this.setValue(this.maxValue || 100)
break
case 'PageUp':
event.preventDefault()
this.setValue(currentValue + (step * 10))
break
case 'PageDown':
event.preventDefault()
this.setValue(currentValue - (step * 10))
break
}
}
// Handle mouse/touch events for custom sliders
handlePointerDown(event) {
event.preventDefault()
this.isDragging = true
this.updateValueFromPointer(event)
document.addEventListener('pointermove', this.boundHandlePointerMove)
document.addEventListener('pointerup', this.boundHandlePointerUp)
}
handlePointerMove(event) {
if (!this.isDragging) return
this.updateValueFromPointer(event)
}
handlePointerUp(event) {
this.isDragging = false
document.removeEventListener('pointermove', this.boundHandlePointerMove)
document.removeEventListener('pointerup', this.boundHandlePointerUp)
}
updateValueFromPointer(event) {
if (!this.hasTrackTarget) return
const rect = this.trackTarget.getBoundingClientRect()
const min = this.minValue || 0
const max = this.maxValue || 100
let percentage
if (this.verticalValue) {
percentage = 1 - ((event.clientY - rect.top) / rect.height)
} else {
percentage = (event.clientX - rect.left) / rect.width
}
percentage = Math.max(0, Math.min(1, percentage))
const value = min + (percentage * (max - min))
// Snap to step
const step = this.stepValue || 1
const snappedValue = Math.round(value / step) * step
this.setValue(snappedValue)
}
dispatchChangeEvent(value) {
const event = new CustomEvent('slider:change', {
detail: {
value: value,
percentage: ((value - (this.minValue || 0)) / ((this.maxValue || 100) - (this.minValue || 0))) * 100,
input: this.inputTarget
}
})
this.element.dispatchEvent(event)
}
// Bind pointer events
initialize() {
this.boundHandlePointerMove = this.handlePointerMove.bind(this)
this.boundHandlePointerUp = this.handlePointerUp.bind(this)
}
}
Installation & Usage
1
Copy the ERB template
Copy the ERB code from the template tab above and paste it into your Rails view file.
2
Add the Stimulus controller
Create the Stimulus controller file in your JavaScript controllers directory.