mcpField
T
tanstack-ranger

tanstack-ranger

github.com/tanstack-skills开发361.3k 次安装39 周前更新
代码仓库

关于 tanstack-ranger

TanStack Ranger 是用于构建无障碍单值、范围和多滑块组件的无头工具集。它处理滑块逻辑,同时由应用控制标记和样式,并提供多个 UI 框架适配器。

主要功能

  1. 01单值与多范围滑块逻辑
  2. 02自定义步进、刻度与插值
  3. 03键盘与 ARIA 无障碍支持
  4. 04React、Vue、Solid、Svelte 和 Angular 适配器

适用场景

  1. 01构建自定义样式的范围滑块
  2. 02创建带多个滑块的控件
  3. 03添加支持键盘操作的滑块

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tanstack-skills/tanstack-skills --skill tanstack-ranger

安装后在 Claude Code 中输入/tanstack-ranger确认已加载。

详细介绍

概述

TanStack Ranger 提供无头工具,用于构建完全无障碍的范围滑块和多范围滑块组件。它负责处理单值、范围和多滑块滑块的所有复杂逻辑,同时让你完全掌控样式和标记。

软件包: @tanstack/react-ranger 核心: @tanstack/ranger-core(与框架无关) 状态: 稳定

安装

npm install @tanstack/react-ranger

核心模式

import { useRanger } from '@tanstack/react-ranger'

function RangeSlider() {
  const [values, setValues] = useState([25, 75])

  const rangerInstance = useRanger({
    getRangerElement: () => rangerRef.current,
    values,
    min: 0,
    max: 100,
    stepSize: 1,
    onChange: (instance) => setValues(instance.sortedValues),
  })

  const rangerRef = useRef<HTMLDivElement>(null)

  return (
    <div
      ref={rangerRef}
      style={{
        position: 'relative',
        height: '8px',
        background: '#ddd',
        borderRadius: '4px',
        width: '100%',
      }}
    >
      {/* Track segments */}
      {rangerInstance.getSteps().map(({ left, width }, i) => (
        <div
          key={i}
          style={{
            position: 'absolute',
            left: `${left}%`,
            width: `${width}%`,
            height: '100%',
            background: i === 1 ? '#3b82f6' : '#ddd',
            borderRadius: '4px',
          }}
        />
      ))}

      {/* Thumbs */}
      {rangerInstance.handles.map((handle, i) => (
        <button
          key={i}
          {...handle.getHandleProps()}
          style={{
            position: 'absolute',
            left: `${handle.getPercentage()}%`,
            transform: 'translateX(-50%)',
            width: '20px',
            height: '20px',
            borderRadius: '50%',
            background: '#3b82f6',
            border: '2px solid white',
            cursor: 'grab',
          }}
        />
      ))}
    </div>
  )
}

Ranger 选项

必需选项
选项类型描述
getRangerElement() => Element | null返回滑块轨道元素
valuesnumber[]当前滑块值
minnumber最小值
maxnumber最大值
onChange(instance) => void值更改时调用
可选选项
选项类型默认值描述
stepSizenumber1值之间的步进增量
stepsnumber[]-自定义步进位置(覆盖 stepSize)
tickSizenumber-刻度标记的大小
ticksnumber[]-自定义刻度位置
interpolatorInterpolatorlinear值插值函数
onDrag(instance) => void-拖动操作期间调用

Ranger 实例 API

// Get sorted values (always ascending order)
rangerInstance.sortedValues: number[]

// Get handles for rendering thumbs
rangerInstance.handles: Handle[]

// Get track segments between handles
rangerInstance.getSteps(): { left: number; width: number }[]

// Get tick marks
rangerInstance.getTicks(): { value: number; percentage: number }[]

// Programmatically set values
rangerInstance.setValues(newValues: number[])

滑块控制柄 API

interface Handle {
  // Get percentage position on track (0-100)
  getPercentage(): number

  // Get the current value
  getValue(): number

  // Get props to spread on handle element
  getHandleProps(): {
    role: 'slider'
    tabIndex: number
    'aria-valuemin': number
    'aria-valuemax': number
    'aria-valuenow': number
    onKeyDown: (e: KeyboardEvent) => void
    onMouseDown: (e: MouseEvent) => void
    onTouchStart: (e: TouchEvent) => void
  }
}

单值滑块

function SingleSlider() {
  const [values, setValues] = useState([50])

  const rangerInstance = useRanger({
    getRangerElement: () => rangerRef.current,
    values,
    min: 0,
    max: 100,
    stepSize: 1,
    onChange: (instance) => setValues(instance.sortedValues),
  })

  const rangerRef = useRef<HTMLDivElement>(null)

  return (
    <div ref={rangerRef} className="slider-track">
      {rangerInstance.handles.map((handle, i) => (
        <button key={i} {...handle.getHandleProps()} className="slider-thumb">
          {handle.getValue()}
        </button>
      ))}
    </div>
  )
}

多范围滑块

function MultiRangeSlider() {
  const [values, setValues] = useState([10, 40, 60, 90])

  const rangerInstance = useRanger({
    getRangerElement: () => rangerRef.current,
    values,
    min: 0,
    max: 100,
    stepSize: 5,
    onChange: (instance) => setValues(instance.sortedValues),
  })

  const rangerRef = useRef<HTMLDivElement>(null)

  return (
    <div ref={rangerRef} className="slider-track">
      {rangerInstance.getSteps().map(({ left, width }, i) => (
        <div
          key={i}
          className={`segment ${i % 2 === 1 ? 'active' : ''}`}
          style={{ left: `${left}%`, width: `${width}%` }}
        />
      ))}
      {rangerInstance.handles.map((handle, i) => (
        <button key={i} {...handle.getHandleProps()} className="slider-thumb" />
      ))}
    </div>
  )
}

自定义步进

const rangerInstance = useRanger({
  getRangerElement: () => rangerRef.current,
  values,
  min: 0,
  max: 100,
  steps: [0, 10, 25, 50, 75, 100], // Only these values allowed
  onChange: (instance) => setValues(instance.sortedValues),
})

刻度标记

function SliderWithTicks() {
  const rangerInstance = useRanger({
    getRangerElement: () => rangerRef.current,
    values,
    min: 0,
    max: 100,
    stepSize: 10,
    ticks: [0, 25, 50, 75, 100],
    onChange: (instance) => setValues(instance.sortedValues),
  })

  return (
    <div>
      <div ref={rangerRef} className="slider-track">
        {/* Handles */}
      </div>
      <div className="tick-container">
        {rangerInstance.getTicks().map((tick, i) => (
          <div
            key={i}
            style={{ left: `${tick.percentage}%` }}
            className="tick"
          >
            <span className="tick-label">{tick.value}</span>
          </div>
        ))}
      </div>
    </div>
  )
}

对数刻度

import { logarithmicInterpolator } from '@tanstack/react-ranger'

const rangerInstance = useRanger({
  getRangerElement: () => rangerRef.current,
  values,
  min: 1,
  max: 1000,
  interpolator: logarithmicInterpolator,
  onChange: (instance) => setValues(instance.sortedValues),
})

无障碍

TanStack Ranger 内置了无障碍支持:

  • 在控制柄上设置 role="slider"
  • aria-valuemin、aria-valuemax、aria-valuenow 属性
  • 键盘导航(方向键、Home、End、Page Up/Down)
  • 焦点管理
// Add aria-label for screen readers
<button
  {...handle.getHandleProps()}
  aria-label={`Value: ${handle.getValue()}`}
/>

受控与非受控

// Controlled (recommended)
const [values, setValues] = useState([50])
const ranger = useRanger({
  values,
  onChange: (instance) => setValues(instance.sortedValues),
  // ...
})

// With validation
const handleChange = (instance) => {
  const [min, max] = instance.sortedValues
  // Ensure minimum gap of 10
  if (max - min >= 10) {
    setValues(instance.sortedValues)
  }
}

样式提示

/* Track */
.slider-track {
  position: relative;
  height: 8px;
  background: #e5e7eb;
  border-radius: 4px;
  width: 100%;
}

/* Active segment */
.segment.active {
  background: #3b82f6;
}

/* Thumb */
.slider-thumb {
  position: absolute;
  transform: translateX(-50%);
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #3b82f6;
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  cursor: grab;
}

.slider-thumb:active {
  cursor: grabbing;
}

.slider-thumb:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

框架适配器

框架软件包状态
React@tanstack/react-ranger稳定
Vue@tanstack/vue-ranger稳定
Solid@tanstack/solid-ranger稳定
Svelte@tanstack/svelte-ranger稳定
Angular@tanstack/angular-ranger稳定
核心@tanstack/ranger-core稳定

最佳实践

  1. 始终使用 onChange 中的 sortedValues — 拖动时控制柄可能交叉
  2. 对 getRangerElement 回调进行记忆化,以避免不必要的重新渲染
  3. 使用语义化 HTML — 将控制柄渲染为 <button> 元素,以支持无障碍访问
  4. 添加 aria-label,说明每个控制柄的用途
  5. 使用 CSS 变换(translateX)定位,而不是使用 left,以提升性能
  6. 在 onChange 中进行验证,以强制执行约束(最小间距、最大范围等)
  7. 使用 onDrag,在拖动操作期间提供实时反馈
  8. 考虑触控目标 — 在移动设备上,控制柄至少应为 44x44px

常见陷阱

  • 忘记在轨道容器上设置 position: relative
  • 使用 values 而不是 sortedValues(控制柄可能交换位置)
  • 未将 getRangerElement 作为回调提供
  • 使用 left 而不是 transform: translateX() 设置滑块位置
  • 忘记处理键盘导航(通过 getHandleProps 内置)
  • 计算位置时未考虑滑块宽度

评论

支持 Markdown