'use client'
import { useState } from 'react'
import { RangeSlider } from '@hina-ui/react'
export default function Demo() {
const [price, setPrice] = useState<[number, number]>([120, 480])
return (
<RangeSlider
value={price}
onValueChange={setPrice}
max={1000}
step={10}
aria-label="价格区间"
className="w-64"
/>
)
}
tsx
用法
import { RangeSlider } from '@hina-ui/react'
ts
范围滑块有两个拇指,value / onValueChange 绑定 [起点, 终点] 的二元数组,填充覆盖两个拇指之间的一段。轨道、拇指、取值标签与 Slider 相同。未声明的属性都会传给根元素,应当用 aria-label 或者 aria-labelledby 给整组命名;两个拇指各自带有「最小值」「最大值」的名称。
当前值:2010 至 2020
'use client'
import { useState } from 'react'
import { RangeSlider, Stack, Text } from '@hina-ui/react'
export default function Demo() {
const [year, setYear] = useState<[number, number]>([2010, 2020])
return (
<Stack gap="sm" className="w-64">
<RangeSlider value={year} onValueChange={setYear} min={1990} max={2026} aria-label="年份" />
<Text size="sm" tone="muted">
当前值:{year[0]} 至 {year[1]}
</Text>
</Stack>
)
}
tsx
示例
方向
dir 支持 ltr 和 rtl,优先于 ConfigProvider 的全局方向配置;未配置时继承外层元素的 dir。切换方向会同步更新轨道、拇指、刻度点与刻度文字。
RTL 下最小值在右、最大值在左;右方向键减小数值,左方向键增大数值,Home / End 仍分别跳到最小值与最大值。
LTR
RTL
'use client'
import { useState } from 'react'
import { RangeSlider, Stack, Text } from '@hina-ui/react'
const marks = [0, 25, 50, 75, 100].map(value => ({ value, label: String(value) }))
export default function Demo() {
const [value, setValue] = useState<[number, number]>([25, 75])
return (
<Stack gap="lg" className="w-72 max-w-full">
{(['ltr', 'rtl'] as const).map(dir => (
<Stack key={dir} gap="xs">
<Text size="sm" tone="muted">
{dir.toUpperCase()}
</Text>
<RangeSlider
value={value}
onValueChange={setValue}
dir={dir}
marks={marks}
step={5}
aria-label={'区间 (' + dir.toUpperCase() + ')'}
/>
</Stack>
))}
</Stack>
)
}
tsx
刻度
marks 与 Slider 相同,step 决定拇指的落点。
'use client'
import { useState } from 'react'
import { RangeSlider } from '@hina-ui/react'
const marks = [
{ value: 0, label: '0' },
{ value: 25 },
{ value: 50, label: '50' },
{ value: 75 },
{ value: 100, label: '100' },
]
export default function Demo() {
const [range, setRange] = useState<[number, number]>([25, 75])
return (
<RangeSlider
value={range}
onValueChange={setRange}
step={25}
marks={marks}
aria-label="区间"
className="w-64"
/>
)
}
tsx
最小间隔
minSteps 规定两个拇指之间至少相隔的步数,拖动或者按键都不能越过。
两个拇指之间至少相隔四步
'use client'
import { useState } from 'react'
import { RangeSlider, Stack, Text } from '@hina-ui/react'
export default function Demo() {
const [hours, setHours] = useState<[number, number]>([9, 18])
return (
<Stack gap="sm" className="w-64">
<RangeSlider
value={hours}
onValueChange={setHours}
max={24}
minSteps={4}
label="always"
aria-label="营业时间"
/>
<Text size="sm" tone="muted">
两个拇指之间至少相隔四步
</Text>
</Stack>
)
}
tsx
取值标签
每个拇指各有一个取值标签,label 与 format 的用法与 Slider 相同。
'use client'
import { useState } from 'react'
import { RangeSlider, Stack } from '@hina-ui/react'
export default function Demo() {
const [discount, setDiscount] = useState<[number, number]>([20, 60])
const [hidden, setHidden] = useState<[number, number]>([30, 70])
return (
<Stack gap="lg" className="w-64 pt-6">
<RangeSlider
value={discount}
onValueChange={setDiscount}
label="always"
format={(v: number) => `${v}%`}
aria-label="折扣区间"
/>
<RangeSlider value={hidden} onValueChange={setHidden} label="none" aria-label="不显示标签" />
</Stack>
)
}
tsx
尺寸
import { RangeSlider, Stack } from '@hina-ui/react'
export default function Demo() {
return (
<Stack gap="sm" className="w-64">
<RangeSlider size="sm" value={[20, 60]} aria-label="小号" />
<RangeSlider size="md" value={[20, 60]} aria-label="中号" />
<RangeSlider size="lg" value={[20, 60]} aria-label="大号" />
</Stack>
)
}
tsx
状态
import { RangeSlider } from '@hina-ui/react'
export default function Demo() {
return <RangeSlider disabled value={[30, 70]} aria-label="已禁用" className="w-64" />
}
tsx
在表单中
放进 FormField 后,标签通过 aria-labelledby 关联到整个滑块组,说明与错误信息由字段渲染;校验规则与提交交给 Form。值是二元数组,区间宽度这类规则写在数组层。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, RangeSlider, Text } from '@hina-ui/react'
const schema = v.object({
budget: v.pipe(
v.tuple([v.number(), v.number()]),
v.check(([low, high]) => high - low >= 200, '预算区间至少相差 200 元'),
),
})
export default function Demo() {
const [values, setValues] = useState({ budget: [300, 400] as [number, number] })
const [saved, setSaved] = useState('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
setSaved(JSON.stringify(data))
}
return (
<Form values={values} rules={schema} className="w-80" onSubmit={save}>
{({ submitting }) => (
<>
<FormField name="budget" label="预算" description="拖动两端设定区间">
<RangeSlider
value={values.budget}
onValueChange={budget => setValues({ ...values, budget })}
min={0}
max={1000}
step={50}
/>
</FormField>
<Button type="submit" loading={submitting} className="self-start">
保存
</Button>
{saved && (
<Text tone="muted" size="sm">
已保存:{saved}
</Text>
)}
</>
)}
</Form>
)
}
tsx
行为
- 点击轨道时,离点击处更近的拇指跳过去;拖动只移动被按住的拇指,两个拇指不会交叉。
- 键盘 Tab 依次落在两个拇指上,方向键只移动持有焦点的那个。
- 拖动过程中值持续更新,松手时另外调用一次
onCommit。
无障碍
- 根元素是
role="group",通过aria-label或者aria-labelledby命名;两个拇指是role="slider",名称取自语言包的「最小值」与「最大值」,并带aria-valuenow、aria-valuemin、aria-valuemax。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | [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 | 是否禁用 |
className | string | — | 追加至根元素的类名 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onValueChange | value: [number, number] | 值变化时,拖动中持续触发 |
onCommit | value: [number, number] | 一次拖动或者按键结束时 |