Range Slider
Multi-thumb APG slider for value ranges with pointer dragging, keyboard, and form association.
| Package | @vizejs/ui/range-slider |
| Maturity | stable |
| Own the source | vize lib pull range-slider |
| Requires | context, controllable-state, slider |
| Aliases | multi-thumb slider, dual slider, price range, range input |
| Covers | WAI-ARIA Multi-Thumb Slider, React Aria RangeSlider, Radix/Reka Slider (multiple thumbs) |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { RangeSlider, RangeSliderTrack, RangeSliderRange, RangeSliderThumb } from "@vizejs/ui/range-slider";
Or copy the source into your project with vize lib pull range-slider (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Controlled two-thumb price range slider with labelled thumbs. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
RangeSlider,
RangeSliderRange,
RangeSliderThumb,
RangeSliderTrack,
} from "@vizejs/ui/range-slider";
const price = ref<readonly number[]>([20, 80]);
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Price range</span>
<RangeSlider v-model="price" name="price" :step="5" :aria-labelledby="labelId">
<RangeSliderTrack>
<RangeSliderRange />
<RangeSliderThumb :index="0" aria-label="Minimum price" />
<RangeSliderThumb :index="1" aria-label="Maximum price" />
</RangeSliderTrack>
</RangeSlider>
<output>${{ price[0] }} – ${{ price[1] }}</output>
</div>
</template>
API
RangeSlider
Source: src/families/form/range-slider/range-slider.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
undefined |
Native form field name. Each thumb submits one hidden value under this name. |
form |
string |
undefined |
Id of a form owner outside the component tree. |
modelValue |
RangeSliderValue |
undefined |
Controlled thumb values. undefined selects uncontrolled behavior. |
defaultValue |
RangeSliderValue |
[min, max] |
Initial uncontrolled thumb values, also restored by form reset. The number of values sets the number of thumbs. |
min |
number |
0 |
Lower bound. |
max |
number |
100 |
Upper bound. Values less than or equal to min are repaired to min + 1. |
step |
number |
1 |
Positive step between selectable values. |
largeStep |
number |
step * 10 |
Positive step used by Page Up and Page Down. |
minStepsBetweenThumbs |
number |
0 |
Minimum number of steps kept between neighboring thumbs. |
orientation |
RangeSliderOrientation |
"horizontal" |
Logical orientation exposed to ARIA, keyboard, and pointer mapping. |
dir |
RangeSliderDirection |
"ltr" |
Text direction. RTL mirrors horizontal pointer mapping and arrow keys. |
disabled |
boolean |
false |
Disable interaction, focus, and native form submission. |
getValueText |
RangeSliderValueText |
undefined |
Formats aria-valuetext for each thumb. |
ariaLabel |
string |
undefined |
Accessible name of the group when no label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the group and every thumb. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe every thumb. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
RangeSliderAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: RangeSliderValue] |
Fired whenever a thumb requests new values, including every drag frame. |
change |
[value: RangeSliderValue, thumbIndex: number, source: RangeSliderChangeSource] |
Fired once a change is committed (key press, pointer release, or API call). |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
RangeSliderSlotState |
Renders the track, range, and one thumb per value with normalized state. |
Exposed
| Member | Type | Description |
|---|---|---|
values |
RangeSliderValue |
Current normalized ascending thumb values. |
percents |
readonly number[] |
Thumb positions from 0 to 100. |
min |
number |
Normalized lower bound. |
max |
number |
Normalized upper bound. |
step |
number |
Normalized positive step. |
largeStep |
number |
Normalized positive large step. |
minDistance |
number |
Normalized minimum gap between neighboring thumbs, in value units. |
orientation |
RangeSliderOrientation |
Logical orientation. |
direction |
RangeSliderDirection |
Text direction. |
disabled |
boolean |
Whether interaction is disabled. |
invalid |
boolean |
Whether the value is announced as invalid. |
activeThumb |
number | null |
Index of the thumb being dragged, or null. |
state |
RangeSliderState |
Stable state token. |
root |
HTMLSpanElement | null |
Rendered root element. |
setThumbValue |
(index: number, value: number) => boolean |
Move one thumb (clamped between its neighbors) and report whether values changed. |
setValue |
(value: RangeSliderValue) => boolean |
Replace every thumb value (normalized and sorted) and report whether values changed. |
focusThumb |
(index: number, options?: FocusOptions) => void |
Focus one thumb. |
reset |
() => boolean |
Restore the default values and report whether they changed. |
RangeSliderTrack
Source: src/families/form/range-slider/range-slider-track.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
Record<string, never> |
Track contents, typically RangeSliderRange and the thumbs. |
RangeSliderRange
Source: src/families/form/range-slider/range-slider-range.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
Record<string, never> |
Optional range contents. |
RangeSliderThumb
Source: src/families/form/range-slider/range-slider-thumb.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
index (required) |
number |
required |
Zero-based index of the value this thumb controls. |
ariaLabel |
string |
undefined |
Accessible name of this thumb, for example "Minimum price". |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
RangeSliderThumbSlotState |
Thumb contents, for example a value tooltip, with the thumb state. |
Behavior
Normative state x input -> outcome table for range-slider.vue,
range-slider-track.vue, range-slider-range.vue, and range-slider-thumb.vue
(@vizejs/ui/range-slider), following the WAI-ARIA APG multi-thumb slider
pattern. The single-thumb native Slider stays in @vizejs/ui/slider;
RangeSlider reuses its bound/step normalization (slider-state.ts) rather than
duplicating it. Every row is proven by the named test in range-slider.test.ts
or range-slider-ssr.test.ts; compile-only assertions live in
range-slider.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| R1 | raw values | normalize | values snap to the grid, sort ascending, keep minStepsBetweenThumbs * step apart, and never cross neighbors; bounds repair like Slider |
normalizes, sorts, spaces, and moves values without crossing neighbors |
| R2 | two thumbs, named | render | labelled role="group"; each thumb is a focusable role="slider" whose aria-valuemin/max are its neighbor-limited range; hidden input per value |
renders a labelled group of APG slider thumbs with range hooks |
| R3 | focused thumb | Arrow / Page / Home / End | arrows step, pages use largeStep (default step * 10), Home/End go to the neighbor-limited bounds; distinct results emit change(values, i, "keyboard") |
keyboard moves one thumb by step, page, and bounds and commits changes |
| R4 | RTL / vertical | arrow keys | RTL mirrors Left/Right; vertical sliders increase with Up | RTL mirrors horizontal arrows and vertical sliders use up for increase |
| R5 | idle | pointer down, move, up on track | the closest thumb jumps, gains focus, drags (clamped to neighbors and track), data-state="dragging"; release emits one change(..., "pointer") |
pointer down moves the closest thumb, drags it, and commits on release |
| R6 | RTL / vertical | pointer | RTL measures from the right edge; vertical measures from the bottom | pointer mapping honors RTL and vertical tracks |
| R7 | controlled | keyboard | emits the request; rendered values stay on modelValue until the parent accepts |
controlled values win until the parent accepts the request |
| R8 | in a form | submit / reset | submits every value under name; form reset restores defaultValue |
submits one value per thumb and restores defaults on form reset |
| R9 | disabled | keyboard / pointer | thumbs stay focusable for discovery with aria-disabled, ignore keyboard and pointer input, and hidden values are disabled |
disabled sliders stay discoverable but ignore input and submit nothing |
| R10 | imperative | expose | setThumbValue, setValue (normalized, thumb count follows), focusThumb, reset, and normalized state |
exposes setThumbValue, setValue, focusThumb, reset, and normalized state |
| R11 | part without provider | setup | throws the stable VIZE_UI_CONTEXT_MISSING: RangeSlider diagnostic |
thumbs and parts require a RangeSlider provider |
| R12 | SSR / hydration | isolated requests | byte-identical markup with CSS custom properties and value text; hydration keeps nodes and thumbs stay interactive | renders byte-identical multi-thumb markup and hydrates without mismatches |
Public extension contract
| Surface | Contract |
|---|---|
| Parts | root, track, range, thumb. |
| Data attributes | data-vize-ui, root data-state / data-orientation / data-dir; thumb data-index, data-active. |
| Boolean hooks | data-disabled, data-invalid (root), data-active (thumb) are "true" only while active. |
| CSS custom properties | Root: --vize-range-slider-range-start, --vize-range-slider-range-end. Thumb: --vize-range-slider-thumb-percent. |
| Slots | Root default slot receives RangeSliderSlotState; render one RangeSliderThumb per values entry. |
The subpath is tree-shakable and ships no CSS; those package contracts are
pinned by distribution.test.ts, check:size, and check:tree-shaking.