radix-vue(reka-ui)Slider 组件完全指南:从多滑块范围选择到无障碍键盘交互
【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue
本文以 docs/content/docs/components/slider.md 为主体,结合当前仓库 packages/core/src/Slider 下的真实源码与测试用例,系统讲解 reka-ui Slider 组件(即 radix-vue 重命名后的发布包名)的安装、部件结构、完整 API 参考、四种高频场景(垂直方向、范围选择、步进、防重叠)、WAI-ARIA 键盘交互、自定义 API 封装以及已知陷阱。读完本文,你将能够在 Vue 3 项目中开箱即用地搭建受控/非受控、支持 RTL、多 thumb 且完全无障碍的滑块组件,并理解其底层实现原理。
组件简介与核心特性
Slider 是一个让用户从给定范围内选取数值的输入组件,是价格区间筛选、音量调节、数值设置等界面中最常见的交互控件。在当前仓库中,发布包名为reka-ui(见 packages/core/package.json),其核心特性包括:
- 可控或非可控:既可以用
v-model受控,也可以用default-value非受控; - 支持多个 Thumb:通过多个
SliderThumb组合出区间(range)选择; - 支持 Thumb 间的最小间距:通过
min-steps-between-thumbs防止 Thumb 值相等或重叠; - 支持点击/触摸轨道直接更新数值;
- 支持从右到左(RTL)方向;
- 完整键盘导航:方向键、PageUp/PageDown、Home/End 一应俱全。
安装与部件构成(Anatomy)
在命令行中安装组件:
npm install reka-ui注意:本文所有示例中的导入语句均使用发布包名
reka-ui。若你直接在本仓库内开发,可从packages/core/src/index.ts查看实际导出。
安装完成后,将各部件组合起来:
<script setup> import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from 'reka-ui' </script> <template> <SliderRoot> <SliderTrack> <SliderRange /> </SliderTrack> <SliderThumb /> </SliderRoot> </template>整个 Slider 由四个部件组成,其层级关系为SliderRoot→SliderTrack→SliderRange,以及挂在轨道上的SliderThumb。从源码结构看,SliderRoot.vue 会根据orientation属性动态选择渲染 SliderHorizontal.vue 或 SliderVertical.vue,并在内部通过provideSliderRootContext向所有子部件共享状态。
API 参考
Root
SliderRoot包含滑块的全部部件。当它被用在<form>中且设置了name属性时,会为每个 Thumb 渲染一个隐藏的input[type=number],确保表单事件能够正确提交(对应源码 SliderRoot.vue 中的VisuallyHiddenInput逻辑,测试见 Slider.test.ts 中的表单提交用例)。
Props
| 名称 | 说明 | 类型 | 必填 | 默认值 |
|---|---|---|---|---|
as | 要渲染成的元素或组件,可被asChild覆盖。 | AsTag \| Component | 否 | "span" |
asChild | 将默认渲染元素替换为传入的子元素,并合并 props 与行为。 | boolean | 否 | - |
defaultValue | 初始渲染时的滑块值,用于非受控场景。 | number[] | 否 | [0] |
dir | 阅读方向;省略时继承全局ConfigProvider配置,否则默认为 LTR。 | "ltr" \| "rtl" | 否 | - |
disabled | 为true时禁止用户与滑块交互。 | boolean | 否 | false |
inverted | 滑块是否视觉反转。 | boolean | 否 | false |
max | 范围的最大值。 | number | 否 | 100 |
min | 范围的最小值。 | number | 否 | 0 |
minStepsBetweenThumbs | 多个 Thumb 之间允许的最小步数。 | number | 否 | 0 |
modelValue | 受控滑块值,可绑定为v-model。 | number[] \| null | 否 | - |
name | 字段名称,随所属表单以 name/value 键值对提交。 | string | 否 | - |
orientation | 滑块方向。 | "vertical" \| "horizontal" | 否 | "horizontal" |
required | 为true时,用户必须在提交所属表单前设置值。 | boolean | 否 | - |
step | 步进间隔。 | number | 否 | 1 |
thumbAlignment | Thumb 对齐方式:contain表示 Thumb 被限制在轨道边界内;overflow表示 Thumb 不受轨道约束,不额外添加偏移。 | "contain" \| "overflow" | 否 | "contain" |
Events
| 名称 | 说明 | 类型 |
|---|---|---|
update:modelValue | 滑块值变化时触发。 | [payload: number[]] |
valueCommit | 一次交互结束时值发生变化时触发,适合在交互结束时只采集一次最终值(如更新后端服务)。 | [payload: number[]] |
Slots
| 名称 | 说明 | 类型 |
|---|---|---|
modelValue | 当前滑块值。 | number[] \| null |
Data Attributes
| 属性 | 值 |
|---|---|
[data-disabled] | 禁用时存在 |
[data-orientation] | vertical|horizontal |
从源码看,update:modelValue在拖动过程中持续触发,而valueCommit仅在交互结束时触发:根组件在pointerdown时把当前值快照存入valuesBeforeSlideStartRef,slide-end事件时比对快照与当前值,若有变化才emits('valueCommit', ...)(见 SliderRoot.vue)。
Track
SliderTrack是容纳SliderRange的轨道,仅包含as与asChild两个 props,并透传data-disabled与data-orientation两个 data attributes(见 SliderTrack.vue)。轨道本身不参与值计算,纯属布局容器,其样式(高度/宽度、背景色)完全由使用者通过 CSS 控制。
Range
SliderRange是滑块上表示当前取值区间的部分,必须放置在SliderTrack内部。它只有as/asChildprops,但会通过注入的上下文自动计算位置:
- 单 Thumb 时,从起点(0%)延伸至当前值对应的百分比;
- 多 Thumb 时,
offsetStart取所有值中最小值的百分比,offsetEnd取100 - max(percentages),从而生成一段区间条(见 SliderRange.vue)。
百分比由 utils.ts 中的convertValueToPercentage计算,即clamp((value - min) / (max - min) * 100, 0, 100)。
Thumb
SliderThumb是用户可拖拽的手柄,可以渲染多个。它同样只有as/asChild两个 props,但实际渲染时(见 SliderThumbImpl.vue)会自带完整的 ARIA 语义:
role="slider",tabindex="0"(未禁用时);aria-valuenow/aria-valuemin/aria-valuemax;aria-orientation;- 自动生成
aria-label:当有 2 个 Thumb 时分别标注 "Minimum" / "Maximum";超过 2 个时标注 "Value 1 of N"(见 utils.ts 的getLabel)。
同时,Thumb 通过Collection机制注册自身元素,根组件据此维护thumbElements数组,并在值变化时自动把焦点移到当前操作的 Thumb 上。SSR 场景下,未挂载且尚无值的 Thumb 会被display: none隐藏,避免水合时位置跳动(见 SliderThumbImpl.vue)。
典型用法示例
垂直方向(Vertical orientation)
通过orientation="vertical"属性创建垂直滑块:
// index.vue <script setup> import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from 'reka-ui' </script> <template> <SliderRoot class="SliderRoot" :default-value="[50]" orientation="vertical" > <SliderTrack class="SliderTrack"> <SliderRange class="SliderRange" /> </SliderTrack> <SliderThumb class="SliderThumb" /> </SliderRoot> </template>/* styles.css */ .SliderRoot { position: relative; display: flex; align-items: center; } .SliderRoot[data-orientation="vertical"] { flex-direction: column; width: 20px; height: 100px; } .SliderTrack { position: relative; flex-grow: 1; background-color: grey; } .SliderTrack[data-orientation="vertical"] { width: 3px; } .SliderRange { position: absolute; background-color: black; } .SliderRange[data-orientation="vertical"] { width: 100%; } .SliderThumb { display: block; width: 20px; height: 20px; background-color: black; }CSS 中利用组件自动输出的[data-orientation]属性选择器区分横/纵布局,这是 reka-ui 各部件统一遵循的样式约定。仓库演示文件 docs/components/demo/Slider/css/index.vue 与styles.css展示了完整可运行的受控版本。
创建范围选择(Create a range)
添加多个 Thumb 和对应的默认值即可组成区间滑块:
// index.vue <script setup> import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from 'reka-ui' </script> <template> <SliderRoot :default-value="[25, 75]"> <SliderTrack> <SliderRange /> </SliderTrack> <SliderThumb /> <SliderThumb /> </SliderRoot> </template>从源码看,多 Thumb 场景下根组件会把新值写入数组后再整体排序(getNextSortedValues,见 utils.ts),保证左侧 Thumb 始终小于右侧 Thumb。
定义步进大小(Define step size)
通过step属性加大步进间隔:
// index.vue <script setup> import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from 'reka-ui' </script> <template> <SliderRoot :default-value="[50]" :step="10" > <SliderTrack> <SliderRange /> </SliderTrack> <SliderThumb /> </SliderRoot> </template>步进逻辑在 SliderRoot.vue 的updateValues中:先将值吸附到最近的step倍数(Math.round((value - min) / step) * step + min),再依据step的小数位数进行舍入(roundValue,见 utils.ts),最后 clamp 到[min, max]。这意味着step可以设为小数(如0.5),精度问题会被正确处理。
防止 Thumb 重叠(Prevent thumb overlap)
使用min-steps-between-thumbs避免 Thumb 取值相等:
// index.vue <script setup> import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from 'reka-ui' </script> <template> <SliderRoot :default-value="[25, 75]" :step="10" :min-steps-between-thumbs="1" > <SliderTrack> <SliderRange /> </SliderTrack> <SliderThumb /> <SliderThumb /> </SliderRoot> </template>底层校验由hasMinStepsBetweenValues完成(见 utils.ts):计算相邻 Thumb 值之间的实际最小步数,若小于minStepsBetweenThumbs * step则拒绝本次更新。因此该值以"步数"为单位,实际最小间距为minStepsBetweenThumbs * step。
无障碍(Accessibility)
组件遵循 WAI-ARIA Slider 设计模式,测试用例 Slider.test.ts 中通过axe无违例断言(toHaveNoViolations)验证了其无障碍达标,并断言了aria-valuenow、aria-valuemin="0"、aria-valuemax="100"、data-disabled等关键属性的输出。
键盘交互
| 按键 | 行为 |
|---|---|
ArrowRight | 按step增加数值 |
ArrowLeft | 按step减小数值 |
ArrowUp | 按step增加数值 |
ArrowDown | 按step减小数值 |
PageUp | 按更大的步长增加数值(×10) |
PageDown | 按更大的步长减小数值(×10) |
Shift + ArrowUp | 按更大的步长增加数值(×10) |
Shift + ArrowDown | 按更大的步长减小数值(×10) |
Home | 将数值设为最小值 |
End | 将数值设为最大值 |
按键的放大倍数(×10)在根组件的step-key-down处理中实现:当按下 Page 键或Shift + 方向键时multiplier = 10(见 SliderRoot.vue)。测试同样验证了pageUp使值 +10、pageDown使值 -10。SliderImpl中还会对方向键、Page 键、Home/End 调用preventDefault(),避免页面滚动(见 SliderImpl.vue)。
反转滑块(Inverted sliders)
当滑块设置inverted后,部分按键行为随之反转,具体取决于orientation:
- 水平滑块(默认)下,
ArrowRight、ArrowLeft、Home、End反转; - 垂直滑块下,
ArrowUp、ArrowDown、PageUp、PageDown、Shift + ArrowUp、Shift + ArrowDown反转。
其实现位于 SliderHorizontal.vue:isSlidingFromLeft由dir与inverted共同决定,进而通过 utils.ts 的BACK_KEYS映射决定哪些按键是"后退"(方向为 -1)。测试用例中反转场景下ArrowRight使值 -1、ArrowLeft使值 +1,与文档描述完全一致(见 Slider.test.ts)。
自定义 API 封装(Custom APIs)
你可以把全部 Slider 部件抽象进自己的组件,封装出自闭合的自定义 API,让使用方只需一行代码:
使用方式
<script setup lang="ts"> import { Slider } from './your-slider' </script> <template> <Slider :default-value="[25]" /> </template>实现
// your-slider.ts export { default as Slider } from 'Slider.vue'<!-- Slider.vue --> <script setup lang="ts"> import type { SliderRootEmits, SliderRootProps } from 'reka-ui' import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from 'reka-ui' const props = defineProps<SliderRootProps>() const emits = defineEmits<SliderRootEmits>() const forward = useForwardPropsEmits(props, emits) </script> <template> <SliderRoot v-slot="{ modelValue }" v-bind="forward"> <SliderTrack> <SliderRange /> </SliderTrack> <SliderThumb v-for="(_, i) in modelValue" :key="i" /> </SliderRoot> </template>要点:
- 使用
SliderRootProps/SliderRootEmits类型让你的自定义组件完整继承官方 props 与事件类型提示; - 使用
useForwardPropsEmits将 props 与 emits 一并透传给内部SliderRoot; - 通过
SliderRoot的默认插槽拿到modelValue,用v-for按数值个数动态渲染 Thumb——这正是组件支持任意数量 Thumb 的关键,也让外层调用者无需关心 Thumb 数量。
已知陷阱(Caveats)
鼠标事件不会被触发
由于实现中的一个已知限制(参见源码 SliderImpl.vue 中pointerdown的preventDefault与指针捕获逻辑),下面这种用法不会按预期工作,@mousedown/@mouseup事件处理器不会触发:
<SliderRoot @mousedown="() => { console.log('onMouseDown') }" @mouseup="() => { console.log('onMouseUp') }" > … </SliderRoot>官方建议改用指针事件(如@pointerdown、@pointerup)。原因有二:一是组件内部基于 Pointer Events 实现拖拽(在 SliderImpl.vue 中通过setPointerCapture/releasePointerCapture处理pointerdown/pointermove/pointerup),鼠标事件会与其冲突;二是指针事件天然跨平台、跨设备(鼠标、触摸、触控笔等所有指针输入类型都会触发),是更现代的推荐方案。
小结
reka-ui 的 Slider 组件以"部件化 + 上下文注入 + Pointer Events + WAI-ARIA"四层架构实现:SliderRoot负责状态与步进/排序/防重叠算法,SliderImpl统一处理键盘与指针事件,SliderHorizontal/Vertical负责几何换算,SliderThumbImpl输出完整 ARIA 语义。借助本文的 API 表格、四个实战示例、键盘交互说明与自定义封装方案,你可以快速在项目中落地一个生产级、可访问、支持 RTL 与多 Thumb 的滑块控件。
【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考