<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider } from '@hina-ui/vue'
const price = ref<[number, number]>([120, 480])
</script>
<template>
<RangeSlider v-model="price" :max="1000" :step="10" aria-label="Price range" class="w-64" />
</template>
Usage
import { RangeSlider } from '@hina-ui/vue'
The range slider has two thumbs; v-model binds a [start, end] pair and the fill covers the span between them. Track, thumbs and value labels are the same as Slider. Attributes it does not declare land on the root, so name the group with aria-label or aria-labelledby; the thumbs carry “Minimum” and “Maximum” names of their own.
Value: 2010 to 2020
<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider, Stack, Text } from '@hina-ui/vue'
const year = ref<[number, number]>([2010, 2020])
</script>
<template>
<Stack gap="sm" class="w-64">
<RangeSlider v-model="year" :min="1990" :max="2026" aria-label="Years" />
<Text size="sm" tone="muted">Value: {{ year[0] }} to {{ year[1] }}</Text>
</Stack>
</template>
Examples
Direction
dir accepts ltr or rtl and takes precedence over Reka’s global direction configuration. Without either setting, it inherits dir from the surrounding elements. Direction changes update the track, thumbs, marks and labels together.
In RTL, the minimum is on the right and the maximum on the left. ArrowRight decreases the value, ArrowLeft increases it, and Home / End still move to the minimum / maximum.
LTR
RTL
<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider, Stack, Text } from '@hina-ui/vue'
const value = ref<[number, number]>([25, 75])
const marks = [0, 25, 50, 75, 100].map(value => ({ value, label: String(value) }))
</script>
<template>
<Stack gap="lg" class="w-72 max-w-full">
<Stack v-for="dir in ['ltr', 'rtl'] as const" :key="dir" gap="xs">
<Text size="sm" tone="muted">{{ dir.toUpperCase() }}</Text>
<RangeSlider
v-model="value"
:dir="dir"
:marks="marks"
:step="5"
:aria-label="'Range (' + dir.toUpperCase() + ')'"
/>
</Stack>
</Stack>
</template>
Marks
marks works as in Slider; step decides where the thumbs land.
<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider } from '@hina-ui/vue'
const range = ref<[number, number]>([25, 75])
const marks = [
{ value: 0, label: '0' },
{ value: 25 },
{ value: 50, label: '50' },
{ value: 75 },
{ value: 100, label: '100' },
]
</script>
<template>
<RangeSlider v-model="range" :step="25" :marks="marks" aria-label="Range" class="w-64" />
</template>
Minimum gap
minSteps keeps the thumbs at least that many steps apart, whether dragging or pressing keys.
The thumbs stay at least four steps apart
<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider, Stack, Text } from '@hina-ui/vue'
const hours = ref<[number, number]>([9, 18])
</script>
<template>
<Stack gap="sm" class="w-64">
<RangeSlider
v-model="hours"
:max="24"
:min-steps="4"
label="always"
aria-label="Opening hours"
/>
<Text size="sm" tone="muted">The thumbs stay at least four steps apart</Text>
</Stack>
</template>
Value labels
Each thumb has its own value label; label and format work as in Slider.
<script setup lang="ts">
import { ref } from 'vue'
import { RangeSlider, Stack } from '@hina-ui/vue'
const discount = ref<[number, number]>([20, 60])
const hidden = ref<[number, number]>([30, 70])
</script>
<template>
<Stack gap="lg" class="w-64 pt-6">
<RangeSlider
v-model="discount"
label="always"
:format="(v: number) => `${v}%`"
aria-label="Discount range"
/>
<RangeSlider v-model="hidden" label="none" aria-label="No label" />
</Stack>
</template>
Sizes
<script setup lang="ts">
import { RangeSlider, Stack } from '@hina-ui/vue'
</script>
<template>
<Stack gap="sm" class="w-64">
<RangeSlider size="sm" :model-value="[20, 60]" aria-label="Small" />
<RangeSlider size="md" :model-value="[20, 60]" aria-label="Medium" />
<RangeSlider size="lg" :model-value="[20, 60]" aria-label="Large" />
</Stack>
</template>
States
<script setup lang="ts">
import { RangeSlider } from '@hina-ui/vue'
</script>
<template>
<RangeSlider disabled :model-value="[30, 70]" aria-label="Disabled" class="w-64" />
</template>
In a form
Inside a FormField the label is linked to the slider group through aria-labelledby, and the description and error message are rendered by the field; validation rules and submission belong to the Form. The value is a pair, so a rule about the width of the range goes on the pair.
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, Form, FormField, RangeSlider, Text } from '@hina-ui/vue'
const schema = v.object({
budget: v.pipe(
v.tuple([v.number(), v.number()]),
v.check(([low, high]) => high - low >= 200, 'The budget range must span at least 200'),
),
})
const values = reactive({ budget: [300, 400] as [number, number] })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
<FormField name="budget" label="Budget" description="Drag both ends to set the range">
<RangeSlider v-model="values.budget" :min="0" :max="1000" :step="50" />
</FormField>
<Button type="submit" :loading="submitting" class="self-start">Save</Button>
<Text v-if="saved" tone="muted" size="sm">Saved: {{ saved }}</Text>
</Form>
</template>
Behaviour
- Clicking the track moves the nearer thumb there; dragging moves only the thumb being held, and the thumbs never cross.
- Tab lands on each thumb in turn; the arrow keys move only the focused one.
- The value updates continuously while dragging, and
commitfires once on release.
Accessibility
- The root is
role="group", named witharia-labeloraria-labelledby; both thumbs arerole="slider", named “Minimum” and “Maximum” from the locale, witharia-valuenow,aria-valueminandaria-valuemax.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | [number, number] | [min, max] | Start and end |
min | number | 0 | Minimum |
max | number | 100 | Maximum |
step | number | 1 | Step |
dir | 'ltr' | 'rtl' | — | Direction; inherited when omitted |
minSteps | number | 0 | Minimum number of steps between the thumbs |
marks | Array<{ value: number; label?: string }> | — | Ticks |
label | 'auto' | 'always' | 'none' | 'auto' | How the value labels show |
format | (value: number) => string | — | Text of the value labels |
size | 'sm' | 'md' | 'lg' | 'md' | Size |
disabled | boolean | false | Whether the slider is disabled |
class | string | — | Classes appended to the root element |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: [number, number] | The value changed, continuously while dragging |
commit | value: [number, number] | A drag or a key press ended |