Range Slider
A slider for picking a single value or a min/max range, with stepped values, tooltips, and full keyboard control.
<!-- Range Slider -->
<!-- An Alpine.js and Tailwind CSS component by https://pinemix.com -->
<div
class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-zinc-200/75 bg-zinc-50 px-4 py-44 dark:border-zinc-700 dark:bg-zinc-950/25"
>
<div
x-data="{
// Customize Range Slider
min: 0,
max: 100,
step: 1,
range: false,
value: 40,
minValue: 25,
maxValue: 75,
showTooltip: true,
prefix: '',
suffix: '',
label: 'Volume',
// Helper variables
dragging: null,
rtl: false,
_dirObserver: null,
// Initial functionality
init() {
this.value = this.clampStep(this.value);
this.minValue = this.clampStep(this.minValue);
this.maxValue = this.clampStep(this.maxValue);
if (this.minValue > this.maxValue) {
[this.minValue, this.maxValue] = [this.maxValue, this.minValue];
}
this.detectRtl();
this._dirObserver = new MutationObserver(() => this.detectRtl());
this._dirObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
},
// Disconnect the dir observer when the component is unmounted
destroy() {
this._dirObserver?.disconnect();
},
// Detect whether the slider is rendered in a right-to-left context
detectRtl() {
this.rtl = getComputedStyle(this.$el).direction === 'rtl';
},
// Clamp a value to the slider bounds and snap it to the nearest step
clampStep(v) {
const snapped = Math.round((v - this.min) / this.step) * this.step + this.min;
return Math.max(this.min, Math.min(this.max, snapped));
},
// Convert a value into its percentage position from the physical left,
// flipped when the slider is in a right-to-left context
toPercent(v) {
if (this.max === this.min) return 0;
const pct = ((v - this.min) / (this.max - this.min)) * 100;
return this.rtl ? 100 - pct : pct;
},
// Format a value for display with the configured prefix and suffix
format(v) {
return this.prefix + v + this.suffix;
},
// Convert a pointer x coordinate to a stepped value, accounting for RTL
positionToValue(clientX) {
const rect = this.$refs.track.getBoundingClientRect();
const offset = this.rtl ? rect.right - clientX : clientX - rect.left;
const ratio = Math.max(0, Math.min(1, offset / rect.width));
return this.clampStep(this.min + ratio * (this.max - this.min));
},
// On pointer down: pick the nearest handle, move it, then start dragging it
onPointerDown(event) {
event.preventDefault();
const v = this.positionToValue(event.clientX);
let handle;
if (!this.range) {
handle = 'single';
} else {
handle = Math.abs(v - this.minValue) <= Math.abs(v - this.maxValue) ? 'min' : 'max';
}
this.dragging = handle;
this.updateValue(v);
this.$nextTick(() => this.$refs[handle + 'Handle']?.focus());
const move = (e) => this.updateValue(this.positionToValue(e.clientX));
const up = () => {
this.dragging = null;
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
},
// Apply a new value to the currently active handle
updateValue(v) {
if (this.dragging === 'single') {
this.value = v;
} else if (this.dragging === 'min') {
this.minValue = Math.min(v, this.maxValue - this.step);
} else if (this.dragging === 'max') {
this.maxValue = Math.max(v, this.minValue + this.step);
}
},
// Keyboard control for a focused handle
onKey(handle, event) {
const dir = this.rtl ? -1 : 1;
const keys = { ArrowRight: dir, ArrowUp: 1, ArrowLeft: -dir, ArrowDown: -1, PageUp: 10, PageDown: -10, Home: 'home', End: 'end' };
if (!(event.key in keys)) return;
event.preventDefault();
const action = keys[event.key];
if (handle === 'single') {
if (action === 'home') this.value = this.min;
else if (action === 'end') this.value = this.max;
else this.value = this.clampStep(this.value + action * this.step);
} else if (handle === 'min') {
const next = action === 'home'
? this.min
: action === 'end'
? this.maxValue - this.step
: this.clampStep(this.minValue + action * this.step);
this.minValue = Math.min(next, this.maxValue - this.step);
} else if (handle === 'max') {
const next = action === 'home'
? this.minValue + this.step
: action === 'end'
? this.max
: this.clampStep(this.maxValue + action * this.step);
this.maxValue = Math.max(next, this.minValue + this.step);
}
}
}"
class="flex w-full max-w-sm flex-col gap-3"
>
<!-- Header -->
<div class="flex items-center justify-between text-sm">
<span
class="font-medium text-zinc-900 dark:text-zinc-100"
x-text="label"
></span>
<span
x-show="!range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
x-text="format(value)"
></span>
<span
x-show="range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
>
<span x-text="format(minValue)"></span>
–
<span x-text="format(maxValue)"></span>
</span>
</div>
<!-- END Header -->
<!-- Slider -->
<div
x-on:pointerdown="onPointerDown($event)"
class="relative flex h-5 touch-none items-center select-none"
>
<!-- Track -->
<div
x-ref="track"
class="relative h-1.5 w-full rounded-full bg-zinc-200 dark:bg-zinc-700"
>
<!-- Single Fill -->
<div
x-show="!range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(min), toPercent(value)) + '%', width: Math.abs(toPercent(value) - toPercent(min)) + '%' }"
></div>
<!-- END Single Fill -->
<!-- Range Fill -->
<div
x-show="range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(minValue), toPercent(maxValue)) + '%', width: Math.abs(toPercent(maxValue) - toPercent(minValue)) + '%' }"
></div>
<!-- END Range Fill -->
</div>
<!-- END Track -->
<!-- Single Handle -->
<button
x-show="!range"
x-ref="singleHandle"
x-on:keydown="onKey('single', $event)"
type="button"
role="slider"
x-bind:aria-label="label"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="value"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'single' && 'scale-110'"
x-bind:style="{ left: toPercent(value) + '%' }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'single' && 'opacity-100'"
x-text="format(value)"
></span>
</button>
<!-- END Single Handle -->
<!-- Range Min Handle -->
<button
x-show="range"
x-ref="minHandle"
x-on:keydown="onKey('min', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' minimum'"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="maxValue"
x-bind:aria-valuenow="minValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'min' && 'scale-110'"
x-bind:style="{ left: toPercent(minValue) + '%', zIndex: dragging === 'min' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'min' && 'opacity-100'"
x-text="format(minValue)"
></span>
</button>
<!-- END Range Min Handle -->
<!-- Range Max Handle -->
<button
x-show="range"
x-ref="maxHandle"
x-on:keydown="onKey('max', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' maximum'"
x-bind:aria-valuemin="minValue"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="maxValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'max' && 'scale-110'"
x-bind:style="{ left: toPercent(maxValue) + '%', zIndex: dragging === 'max' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'max' && 'opacity-100'"
x-text="format(maxValue)"
></span>
</button>
<!-- END Range Max Handle -->
</div>
<!-- END Slider -->
<!-- Min/Max Labels -->
<div
class="flex justify-between text-xs text-zinc-500 tabular-nums dark:text-zinc-400"
>
<span x-text="format(min)"></span>
<span x-text="format(max)"></span>
</div>
<!-- END Min/Max Labels -->
</div>
</div>
<!-- END Range Slider -->
Dual Handle
<!-- Range Slider: Dual Handle -->
<!-- An Alpine.js and Tailwind CSS component by https://pinemix.com -->
<div
class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-zinc-200/75 bg-zinc-50 px-4 py-44 dark:border-zinc-700 dark:bg-zinc-950/25"
>
<div
x-data="{
// Customize Range Slider
min: 0,
max: 100,
step: 1,
range: true,
value: 40,
minValue: 25,
maxValue: 75,
showTooltip: true,
prefix: '',
suffix: '',
label: 'Range',
// Helper variables
dragging: null,
rtl: false,
_dirObserver: null,
// Initial functionality
init() {
this.value = this.clampStep(this.value);
this.minValue = this.clampStep(this.minValue);
this.maxValue = this.clampStep(this.maxValue);
if (this.minValue > this.maxValue) {
[this.minValue, this.maxValue] = [this.maxValue, this.minValue];
}
this.detectRtl();
this._dirObserver = new MutationObserver(() => this.detectRtl());
this._dirObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
},
// Disconnect the dir observer when the component is unmounted
destroy() {
this._dirObserver?.disconnect();
},
// Detect whether the slider is rendered in a right-to-left context
detectRtl() {
this.rtl = getComputedStyle(this.$el).direction === 'rtl';
},
// Clamp a value to the slider bounds and snap it to the nearest step
clampStep(v) {
const snapped = Math.round((v - this.min) / this.step) * this.step + this.min;
return Math.max(this.min, Math.min(this.max, snapped));
},
// Convert a value into its percentage position from the physical left,
// flipped when the slider is in a right-to-left context
toPercent(v) {
if (this.max === this.min) return 0;
const pct = ((v - this.min) / (this.max - this.min)) * 100;
return this.rtl ? 100 - pct : pct;
},
// Format a value for display with the configured prefix and suffix
format(v) {
return this.prefix + v + this.suffix;
},
// Convert a pointer x coordinate to a stepped value, accounting for RTL
positionToValue(clientX) {
const rect = this.$refs.track.getBoundingClientRect();
const offset = this.rtl ? rect.right - clientX : clientX - rect.left;
const ratio = Math.max(0, Math.min(1, offset / rect.width));
return this.clampStep(this.min + ratio * (this.max - this.min));
},
// On pointer down: pick the nearest handle, move it, then start dragging it
onPointerDown(event) {
event.preventDefault();
const v = this.positionToValue(event.clientX);
let handle;
if (!this.range) {
handle = 'single';
} else {
handle = Math.abs(v - this.minValue) <= Math.abs(v - this.maxValue) ? 'min' : 'max';
}
this.dragging = handle;
this.updateValue(v);
this.$nextTick(() => this.$refs[handle + 'Handle']?.focus());
const move = (e) => this.updateValue(this.positionToValue(e.clientX));
const up = () => {
this.dragging = null;
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
},
// Apply a new value to the currently active handle
updateValue(v) {
if (this.dragging === 'single') {
this.value = v;
} else if (this.dragging === 'min') {
this.minValue = Math.min(v, this.maxValue - this.step);
} else if (this.dragging === 'max') {
this.maxValue = Math.max(v, this.minValue + this.step);
}
},
// Keyboard control for a focused handle
onKey(handle, event) {
const dir = this.rtl ? -1 : 1;
const keys = { ArrowRight: dir, ArrowUp: 1, ArrowLeft: -dir, ArrowDown: -1, PageUp: 10, PageDown: -10, Home: 'home', End: 'end' };
if (!(event.key in keys)) return;
event.preventDefault();
const action = keys[event.key];
if (handle === 'single') {
if (action === 'home') this.value = this.min;
else if (action === 'end') this.value = this.max;
else this.value = this.clampStep(this.value + action * this.step);
} else if (handle === 'min') {
const next = action === 'home'
? this.min
: action === 'end'
? this.maxValue - this.step
: this.clampStep(this.minValue + action * this.step);
this.minValue = Math.min(next, this.maxValue - this.step);
} else if (handle === 'max') {
const next = action === 'home'
? this.minValue + this.step
: action === 'end'
? this.max
: this.clampStep(this.maxValue + action * this.step);
this.maxValue = Math.max(next, this.minValue + this.step);
}
}
}"
class="flex w-full max-w-sm flex-col gap-3"
>
<!-- Header -->
<div class="flex items-center justify-between text-sm">
<span
class="font-medium text-zinc-900 dark:text-zinc-100"
x-text="label"
></span>
<span
x-show="!range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
x-text="format(value)"
></span>
<span
x-show="range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
>
<span x-text="format(minValue)"></span>
–
<span x-text="format(maxValue)"></span>
</span>
</div>
<!-- END Header -->
<!-- Slider -->
<div
x-on:pointerdown="onPointerDown($event)"
class="relative flex h-5 touch-none items-center select-none"
>
<!-- Track -->
<div
x-ref="track"
class="relative h-1.5 w-full rounded-full bg-zinc-200 dark:bg-zinc-700"
>
<!-- Single Fill -->
<div
x-show="!range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(min), toPercent(value)) + '%', width: Math.abs(toPercent(value) - toPercent(min)) + '%' }"
></div>
<!-- END Single Fill -->
<!-- Range Fill -->
<div
x-show="range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(minValue), toPercent(maxValue)) + '%', width: Math.abs(toPercent(maxValue) - toPercent(minValue)) + '%' }"
></div>
<!-- END Range Fill -->
</div>
<!-- END Track -->
<!-- Single Handle -->
<button
x-show="!range"
x-ref="singleHandle"
x-on:keydown="onKey('single', $event)"
type="button"
role="slider"
x-bind:aria-label="label"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="value"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'single' && 'scale-110'"
x-bind:style="{ left: toPercent(value) + '%' }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'single' && 'opacity-100'"
x-text="format(value)"
></span>
</button>
<!-- END Single Handle -->
<!-- Range Min Handle -->
<button
x-show="range"
x-ref="minHandle"
x-on:keydown="onKey('min', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' minimum'"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="maxValue"
x-bind:aria-valuenow="minValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'min' && 'scale-110'"
x-bind:style="{ left: toPercent(minValue) + '%', zIndex: dragging === 'min' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'min' && 'opacity-100'"
x-text="format(minValue)"
></span>
</button>
<!-- END Range Min Handle -->
<!-- Range Max Handle -->
<button
x-show="range"
x-ref="maxHandle"
x-on:keydown="onKey('max', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' maximum'"
x-bind:aria-valuemin="minValue"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="maxValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'max' && 'scale-110'"
x-bind:style="{ left: toPercent(maxValue) + '%', zIndex: dragging === 'max' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'max' && 'opacity-100'"
x-text="format(maxValue)"
></span>
</button>
<!-- END Range Max Handle -->
</div>
<!-- END Slider -->
<!-- Min/Max Labels -->
<div
class="flex justify-between text-xs text-zinc-500 tabular-nums dark:text-zinc-400"
>
<span x-text="format(min)"></span>
<span x-text="format(max)"></span>
</div>
<!-- END Min/Max Labels -->
</div>
</div>
<!-- END Range Slider: Dual Handle -->
Price Range
<!-- Range Slider: Price Range -->
<!-- An Alpine.js and Tailwind CSS component by https://pinemix.com -->
<div
class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-zinc-200/75 bg-zinc-50 px-4 py-44 dark:border-zinc-700 dark:bg-zinc-950/25"
>
<div
x-data="{
// Customize Range Slider
min: 0,
max: 1000,
step: 50,
range: true,
value: 500,
minValue: 200,
maxValue: 800,
showTooltip: true,
prefix: '$',
suffix: '',
label: 'Price',
// Helper variables
dragging: null,
rtl: false,
_dirObserver: null,
// Initial functionality
init() {
this.value = this.clampStep(this.value);
this.minValue = this.clampStep(this.minValue);
this.maxValue = this.clampStep(this.maxValue);
if (this.minValue > this.maxValue) {
[this.minValue, this.maxValue] = [this.maxValue, this.minValue];
}
this.detectRtl();
this._dirObserver = new MutationObserver(() => this.detectRtl());
this._dirObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
},
// Disconnect the dir observer when the component is unmounted
destroy() {
this._dirObserver?.disconnect();
},
// Detect whether the slider is rendered in a right-to-left context
detectRtl() {
this.rtl = getComputedStyle(this.$el).direction === 'rtl';
},
// Clamp a value to the slider bounds and snap it to the nearest step
clampStep(v) {
const snapped = Math.round((v - this.min) / this.step) * this.step + this.min;
return Math.max(this.min, Math.min(this.max, snapped));
},
// Convert a value into its percentage position from the physical left,
// flipped when the slider is in a right-to-left context
toPercent(v) {
if (this.max === this.min) return 0;
const pct = ((v - this.min) / (this.max - this.min)) * 100;
return this.rtl ? 100 - pct : pct;
},
// Format a value for display with the configured prefix and suffix
format(v) {
return this.prefix + v + this.suffix;
},
// Convert a pointer x coordinate to a stepped value, accounting for RTL
positionToValue(clientX) {
const rect = this.$refs.track.getBoundingClientRect();
const offset = this.rtl ? rect.right - clientX : clientX - rect.left;
const ratio = Math.max(0, Math.min(1, offset / rect.width));
return this.clampStep(this.min + ratio * (this.max - this.min));
},
// On pointer down: pick the nearest handle, move it, then start dragging it
onPointerDown(event) {
event.preventDefault();
const v = this.positionToValue(event.clientX);
let handle;
if (!this.range) {
handle = 'single';
} else {
handle = Math.abs(v - this.minValue) <= Math.abs(v - this.maxValue) ? 'min' : 'max';
}
this.dragging = handle;
this.updateValue(v);
this.$nextTick(() => this.$refs[handle + 'Handle']?.focus());
const move = (e) => this.updateValue(this.positionToValue(e.clientX));
const up = () => {
this.dragging = null;
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
},
// Apply a new value to the currently active handle
updateValue(v) {
if (this.dragging === 'single') {
this.value = v;
} else if (this.dragging === 'min') {
this.minValue = Math.min(v, this.maxValue - this.step);
} else if (this.dragging === 'max') {
this.maxValue = Math.max(v, this.minValue + this.step);
}
},
// Keyboard control for a focused handle
onKey(handle, event) {
const dir = this.rtl ? -1 : 1;
const keys = { ArrowRight: dir, ArrowUp: 1, ArrowLeft: -dir, ArrowDown: -1, PageUp: 10, PageDown: -10, Home: 'home', End: 'end' };
if (!(event.key in keys)) return;
event.preventDefault();
const action = keys[event.key];
if (handle === 'single') {
if (action === 'home') this.value = this.min;
else if (action === 'end') this.value = this.max;
else this.value = this.clampStep(this.value + action * this.step);
} else if (handle === 'min') {
const next = action === 'home'
? this.min
: action === 'end'
? this.maxValue - this.step
: this.clampStep(this.minValue + action * this.step);
this.minValue = Math.min(next, this.maxValue - this.step);
} else if (handle === 'max') {
const next = action === 'home'
? this.minValue + this.step
: action === 'end'
? this.max
: this.clampStep(this.maxValue + action * this.step);
this.maxValue = Math.max(next, this.minValue + this.step);
}
}
}"
class="flex w-full max-w-sm flex-col gap-3"
>
<!-- Header -->
<div class="flex items-center justify-between text-sm">
<span
class="font-medium text-zinc-900 dark:text-zinc-100"
x-text="label"
></span>
<span
x-show="!range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
x-text="format(value)"
></span>
<span
x-show="range"
class="font-semibold text-zinc-700 tabular-nums dark:text-zinc-300"
>
<span x-text="format(minValue)"></span>
–
<span x-text="format(maxValue)"></span>
</span>
</div>
<!-- END Header -->
<!-- Slider -->
<div
x-on:pointerdown="onPointerDown($event)"
class="relative flex h-5 touch-none items-center select-none"
>
<!-- Track -->
<div
x-ref="track"
class="relative h-1.5 w-full rounded-full bg-zinc-200 dark:bg-zinc-700"
>
<!-- Single Fill -->
<div
x-show="!range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(min), toPercent(value)) + '%', width: Math.abs(toPercent(value) - toPercent(min)) + '%' }"
></div>
<!-- END Single Fill -->
<!-- Range Fill -->
<div
x-show="range"
class="absolute top-0 h-full rounded-full bg-teal-500"
x-bind:style="{ left: Math.min(toPercent(minValue), toPercent(maxValue)) + '%', width: Math.abs(toPercent(maxValue) - toPercent(minValue)) + '%' }"
></div>
<!-- END Range Fill -->
</div>
<!-- END Track -->
<!-- Single Handle -->
<button
x-show="!range"
x-ref="singleHandle"
x-on:keydown="onKey('single', $event)"
type="button"
role="slider"
x-bind:aria-label="label"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="value"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'single' && 'scale-110'"
x-bind:style="{ left: toPercent(value) + '%' }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'single' && 'opacity-100'"
x-text="format(value)"
></span>
</button>
<!-- END Single Handle -->
<!-- Range Min Handle -->
<button
x-show="range"
x-ref="minHandle"
x-on:keydown="onKey('min', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' minimum'"
x-bind:aria-valuemin="min"
x-bind:aria-valuemax="maxValue"
x-bind:aria-valuenow="minValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'min' && 'scale-110'"
x-bind:style="{ left: toPercent(minValue) + '%', zIndex: dragging === 'min' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'min' && 'opacity-100'"
x-text="format(minValue)"
></span>
</button>
<!-- END Range Min Handle -->
<!-- Range Max Handle -->
<button
x-show="range"
x-ref="maxHandle"
x-on:keydown="onKey('max', $event)"
type="button"
role="slider"
x-bind:aria-label="label + ' maximum'"
x-bind:aria-valuemin="minValue"
x-bind:aria-valuemax="max"
x-bind:aria-valuenow="maxValue"
class="group absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-sm transition-transform hover:scale-110 focus:ring-3 focus:ring-teal-500/30 focus:outline-hidden dark:border-zinc-900"
x-bind:class="dragging === 'max' && 'scale-110'"
x-bind:style="{ left: toPercent(maxValue) + '%', zIndex: dragging === 'max' ? 2 : 1 }"
>
<span
x-show="showTooltip"
x-cloak
class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md bg-zinc-900 px-2 py-1 text-xs font-semibold whitespace-nowrap text-white tabular-nums opacity-0 transition-opacity group-hover:opacity-100 group-focus:opacity-100 dark:bg-zinc-50 dark:text-zinc-900"
x-bind:class="dragging === 'max' && 'opacity-100'"
x-text="format(maxValue)"
></span>
</button>
<!-- END Range Max Handle -->
</div>
<!-- END Slider -->
<!-- Min/Max Labels -->
<div
class="flex justify-between text-xs text-zinc-500 tabular-nums dark:text-zinc-400"
>
<span x-text="format(min)"></span>
<span x-text="format(max)"></span>
</div>
<!-- END Min/Max Labels -->
</div>
</div>
<!-- END Range Slider: Price Range -->
Props
The available data properties for this component.
| Property | Default | Description |
|---|---|---|
| min | 0 | The minimum value of the slider |
| max | 100 | The maximum value of the slider |
| step | 1 | The increment between selectable values |
| range | false | If set to 'true', the slider uses two handles to pick a min/max range |
| value | 40 | The current value when 'range' is set to 'false' |
| minValue | 25 | The current lower bound when 'range' is set to 'true' |
| maxValue | 75 | The current upper bound when 'range' is set to 'true' |
| showTooltip | true | Sets the visibility of the value tooltip above each handle |
| prefix | '' | Text prepended to the displayed value, for example '$' |
| suffix | '' | Text appended to the displayed value, for example '%' |
| label | '' | The text label shown above the slider |
About this component
Designed with
Tailkit
2,000+ Tailwind CSS code snippets for HTML, React, Vue.js and Alpine.js. AI-powered development with MCP Server.
Unlock 15+ free templates
Join our pixelcave newsletter to get them now & we'll also keep you updated about any new Pinemix components!