RangeSlider 范围滑块

在数值范围内拖出一段区间。

<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="价格区间" class="w-64" />
</template>
vue

用法

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

范围滑块有两个拇指,v-model 绑定 [起点, 终点] 的二元数组,填充覆盖两个拇指之间的一段。轨道、拇指、取值标签与 Slider 相同。未声明的属性都会传给根元素,应当用 aria-label 或者 aria-labelledby 给整组命名;两个拇指各自带有「最小值」「最大值」的名称。

当前值:2010 至 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="年份" />
    <Text size="sm" tone="muted">当前值:{{ year[0] }} 至 {{ year[1] }}</Text>
  </Stack>
</template>
vue

示例

方向

dir 支持 ltr 和 rtl,优先于 Reka 的全局方向配置;未配置时继承外层元素的 dir。切换方向会同步更新轨道、拇指、刻度点与刻度文字。

RTL 下最小值在右、最大值在左;右方向键减小数值,左方向键增大数值,Home / End 仍分别跳到最小值与最大值。

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="'区间 (' + dir.toUpperCase() + ')'"
      />
    </Stack>
  </Stack>
</template>
vue

刻度

marks 与 Slider 相同,step 决定拇指的落点。

<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="区间" class="w-64" />
</template>
vue

最小间隔

minSteps 规定两个拇指之间至少相隔的步数,拖动或者按键都不能越过。

两个拇指之间至少相隔四步

<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="营业时间" />
    <Text size="sm" tone="muted">两个拇指之间至少相隔四步</Text>
  </Stack>
</template>
vue

取值标签

每个拇指各有一个取值标签,label 与 format 的用法与 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="折扣区间"
    />
    <RangeSlider v-model="hidden" label="none" aria-label="不显示标签" />
  </Stack>
</template>
vue

尺寸

<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="小号" />
    <RangeSlider size="md" :model-value="[20, 60]" aria-label="中号" />
    <RangeSlider size="lg" :model-value="[20, 60]" aria-label="大号" />
  </Stack>
</template>
vue

状态

<script setup lang="ts">
  import { RangeSlider } from '@hina-ui/vue'
</script>

<template>
  <RangeSlider disabled :model-value="[30, 70]" aria-label="已禁用" class="w-64" />
</template>
vue

在表单中

放进 FormField 后,标签通过 aria-labelledby 关联到整个滑块组,说明与错误信息由字段渲染;校验规则与提交交给 Form。值是二元数组,区间宽度这类规则写在数组层。

拖动两端设定区间

<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, '预算区间至少相差 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="预算" description="拖动两端设定区间">
      <RangeSlider v-model="values.budget" :min="0" :max="1000" :step="50" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">保存</Button>
    <Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
  </Form>
</template>
vue

行为

  • 点击轨道时,离点击处更近的拇指跳过去;拖动只移动被按住的拇指,两个拇指不会交叉。
  • 键盘 Tab 依次落在两个拇指上,方向键只移动持有焦点的那个。
  • 拖动过程中值持续更新,松手时另发一次 commit。

无障碍

  • 根元素是 role="group",通过 aria-label 或者 aria-labelledby 命名;两个拇指是 role="slider",名称取自语言包的「最小值」与「最大值」,并带 aria-valuenow、aria-valuemin、aria-valuemax。

API

Props

属性
类型
默认值
说明
modelValue
[number, number]
[min, max]
起点与终点
min
number
0
最小值
max
number
100
最大值
step
number
1
步长
dir
'ltr' | 'rtl'
—
方向,未指定时继承
minSteps
number
0
两个拇指之间至少相隔的步数
marks
Array<{ value: number; label?: string }>
—
刻度
label
'auto' | 'always' | 'none'
'auto'
取值标签的显示方式
format
(value: number) => string
—
取值标签的文字
size
'sm' | 'md' | 'lg'
'md'
尺寸
disabled
boolean
false
是否禁用
class
string
—
追加至根元素的类名

事件

事件
参数
说明
update:modelValue
value: [number, number]
值变化时,拖动中持续触发
commit
value: [number, number]
一次拖动或者按键结束时