tanstack-ranger について
TanStack Rangerは、単一値・範囲・複数ハンドルのスライダーを構築するためのヘッドレスツールキットです。スライダーのロジックを提供し、マークアップとスタイルはアプリ側で制御できます。複数のUIフレームワーク向けアダプターもあります。
主な機能
- 01単一値・複数範囲のスライダーロジック
- 02カスタムステップ、目盛り、補間
- 03キーボードとARIAのアクセシビリティ対応
- 04React、Vue、Solid、Svelte、Angular用アダプター
活用シーン
- 01独自スタイルの範囲スライダーの構築
- 02複数ハンドルを持つスライダーの作成
- 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 | スライダーのトラック要素を返します |
values | number[] | 現在のつまみの値 |
min | number | 最小値 |
max | number | 最大値 |
onChange | (instance) => void | 値が変更されたときに呼び出されます |
任意
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
stepSize | number | 1 | 値の間のステップ幅 |
steps | number[] | - | カスタムのステップ位置(stepSize を上書きします) |
tickSize | number | - | 目盛りのサイズ |
ticks | number[] | - | カスタムの目盛り位置 |
interpolator | Interpolator | linear | 値の補間関数 |
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 | 安定版 |
| Core | @tanstack/ranger-core | 安定版 |
ベストプラクティス
- onChange では必ず
sortedValuesを使用します。ドラッグ中につまみが交差する場合に対応できます getRangerElementコールバックをメモ化します。不要な再レンダリングを防げます- セマンティックな HTML を使用します。アクセシビリティのため、ハンドルは
<button>要素としてレンダリングします - 各ハンドルの目的を説明する
aria-labelを追加します - パフォーマンス向上のため、位置指定には
leftではなく CSS トランスフォーム(translateX)を使用します - 制約(最小間隔、最大範囲など)を適用するため、onChange で値を検証します
- ドラッグ操作中にリアルタイムで反応させるには、
onDragを使用します - タッチ領域を考慮します。モバイルではハンドルを少なくとも 44x44px にします
よくある落とし穴
- トラックコンテナーに
position: relativeを設定し忘れる sortedValuesではなくvaluesを使用する(ハンドルの位置が入れ替わることがあります)getRangerElementをコールバックとして渡さないtransform: translateX()ではなくleftでつまみの位置を設定する- キーボード操作への対応を忘れる(
getHandlePropsで組み込み済み) - 位置の計算でつまみの幅を考慮しない
コメント
Markdown 対応