RangeSlider

Picks a range within bounds with two thumbs.

<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>
vue

Usage

import { RangeSlider } from '@hina-ui/vue'
ts

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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.

Drag both ends to set the range

<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>
vue

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 commit fires once on release.

Accessibility

  • The root is role="group", named with aria-label or aria-labelledby; both thumbs are role="slider", named “Minimum” and “Maximum” from the locale, with aria-valuenow, aria-valuemin and aria-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