wts-range-slider
v1.0.1
Published
Dependency-free, accessible range slider for JavaScript, Angular, React, Vue, Svelte, and Web Components.
Maintainers
Readme
wts-range-slider
A dependency-free, accessible range slider for plain JavaScript and any framework that can provide a DOM element. It supports single and dual values, steps, ticks, pointer input, keyboard input, CSS custom properties, SSR-safe module imports, and an optional Web Component.
Install
npm install wts-range-sliderPlain JavaScript
<div id="price-range"></div>import { RangeSlider } from 'wts-range-slider';
const slider = new RangeSlider({
element: '#price-range',
range: { min: 0, max: 1000 },
value: { start: 100, end: 700 },
step: 10,
minDistance: 100,
onInput({ value, source }) {
console.log('live', value, source);
},
onCommit({ value, source }) {
console.log('committed', value, source);
},
});
slider.setValue(
{ start: 200, end: 800 },
{ emitInput: true, emitChange: true },
);
slider.disable();
console.log(slider.getState());
slider.enable();
slider.destroy();The existing constructor API is retained. sliderBarClickAble is still
accepted, although new code should use barClickable.
Web Component
Importing the element entry registers <wts-range-slider>.
import 'wts-range-slider/element';<wts-range-slider
min="0"
max="1000"
start="100"
end="700"
step="10"
min-distance="100"
aria-label="Price range"
></wts-range-slider>const element = document.querySelector('wts-range-slider');
element.addEventListener('range-slider-input', (event) => {
console.log(event.detail.value);
});
element.value = { start: 200, end: 800 };Framework lifecycle
Create the controller only after the element has mounted, then destroy it in the framework cleanup hook.
Angular
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
ViewChild,
} from '@angular/core';
import { RangeSlider } from 'wts-range-slider';
@Component({
selector: 'app-price-range',
template: '<div #sliderHost></div>',
})
export class PriceRangeComponent implements AfterViewInit, OnDestroy {
@ViewChild('sliderHost', { static: true })
host!: ElementRef<HTMLElement>;
private slider?: RangeSlider;
ngAfterViewInit() {
this.slider = new RangeSlider({
element: this.host.nativeElement,
range: { min: 0, max: 1000 },
value: { start: 100, end: 700 },
});
}
ngOnDestroy() {
this.slider?.destroy();
}
}React
import { useEffect, useRef } from 'react';
import { RangeSlider } from 'wts-range-slider';
export function PriceRange() {
const host = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!host.current) return;
const slider = new RangeSlider({
element: host.current,
range: { min: 0, max: 1000 },
value: { start: 100, end: 700 },
});
return () => slider.destroy();
}, []);
return <div ref={host} />;
}Vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, useTemplateRef } from 'vue';
import { RangeSlider } from 'wts-range-slider';
const host = useTemplateRef<HTMLElement>('host');
let slider: RangeSlider | undefined;
onMounted(() => {
slider = new RangeSlider({
element: host.value!,
range: { min: 0, max: 1000 },
value: { start: 100, end: 700 },
});
});
onBeforeUnmount(() => slider?.destroy());
</script>
<template>
<div ref="host" />
</template>Events
The target element emits framework-neutral DOM events:
range-slider-inputwhile the value changes.range-slider-changewhen pointer, keyboard, bar, or tick interaction commits.
Both are bubbling, composed CustomEvents with:
{
value: number | { start: number; end: number };
previousValue: number | { start: number; end: number };
thumb: 'single' | 'start' | 'end';
source: 'keyboard' | 'pointer' | 'programmatic' | 'tick' | 'track';
}The target also emits native bubbling input and change events, making it
easy to integrate with generic DOM and form-oriented tooling. Use onInput
for continuous updates and onCommit for completed interactions.
Options
| Option | Type | Default |
| --- | --- | --- |
| element | Element \| ShadowRoot \| string | required |
| range | { min: number; max: number } | required |
| value | number \| { start: number; end: number } | required |
| step | number | 1 |
| minDistance | number | 0 |
| barClickable | boolean | true |
| disabled | boolean | false |
| showRangeLabel | boolean | true |
| showThumbLabel | boolean \| object | true |
| tick | RangeSliderTickOptions | disabled |
| ariaLabel | string | "Range slider" |
| injectStyles | boolean | true |
| onChange | (value) => void | — |
| onInput | (detail) => void | — |
| onCommit | (detail) => void | — |
transformLabel, transformThumbLabel, and transformTickLabel customize
visible text. Their results are assigned as text, not HTML.
Controller API
getValue()returns a defensive copy of the current value.setValue(value, emit?)updates the value and can selectively emit input and change events.getState()returns the value, range, step, minimum distance, and disabled state.setOptions(options)updates configuration without replacing the host.enable(),disable(), andsetDisabled(value)update interaction state.refresh()recalculates rendered positions anddestroy()removes all DOM and listeners.
Styling
Styles are injected by default. For a shared stylesheet instead:
import 'wts-range-slider/styles.css';
new RangeSlider({
element: '#price-range',
injectStyles: false,
range: { min: 0, max: 100 },
value: 50,
});Theme an instance with CSS variables:
#price-range {
--wts-range-slider-accent: #7c3aed;
--wts-range-slider-thumb-background: #4c1d95;
--wts-range-slider-bar-height: 6px;
--wts-range-slider-thumb-size: 24px;
}SSR
Both package entries can be imported during server rendering. Construct
RangeSlider only in a browser mount/effect hook, because rendering the
control requires a DOM target.
