tanstack-ranger

TanStack Ranger provides headless utilities for building fully accessible range and multi-range slider components. It handles all the complex logic for single value, range, and multi-thumb sliders while giving you complete control over styling and markup.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-ranger" with this command: npx skills add tanstack-skills/tanstack-skills/tanstack-skills-tanstack-skills-tanstack-ranger

Overview

TanStack Ranger provides headless utilities for building fully accessible range and multi-range slider components. It handles all the complex logic for single value, range, and multi-thumb sliders while giving you complete control over styling and markup.

Package: @tanstack/react-ranger

Core: @tanstack/ranger-core (framework-agnostic) Status: Stable

Installation

npm install @tanstack/react-ranger

Core Pattern

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) => (
    &#x3C;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',
      }}
    />
  ))}
&#x3C;/div>

) }

Ranger Options

Required

Option Type Description

getRangerElement

() => Element | null

Returns the slider track element

values

number[]

Current thumb values

min

number

Minimum value

max

number

Maximum value

onChange

(instance) => void

Called when values change

Optional

Option Type Default Description

stepSize

number

1

Step increment between values

steps

number[]

Custom step positions (overrides stepSize)

tickSize

number

Size of tick marks

ticks

number[]

Custom tick positions

interpolator

Interpolator

linear Value interpolation function

onDrag

(instance) => void

Called during drag operations

Ranger Instance 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[])

Handle 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 } }

Single Value Slider

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> ) }

Multi-Range Slider

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> ) }

Custom Steps

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), })

Tick Marks

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> ) }

Logarithmic Scale

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

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

Accessibility

TanStack Ranger provides built-in accessibility:

  • role="slider" on handles

  • aria-valuemin , aria-valuemax , aria-valuenow attributes

  • Keyboard navigation (Arrow keys, Home, End, Page Up/Down)

  • Focus management

// Add aria-label for screen readers <button {...handle.getHandleProps()} aria-label={Value: ${handle.getValue()}} />

Controlled vs Uncontrolled

// 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) } }

Styling Tips

/* 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); }

Framework Adapters

Framework Package Status

React @tanstack/react-ranger

Stable

Vue @tanstack/vue-ranger

Stable

Solid @tanstack/solid-ranger

Stable

Svelte @tanstack/svelte-ranger

Stable

Angular @tanstack/angular-ranger

Stable

Core @tanstack/ranger-core

Stable

Best Practices

  • Always use sortedValues from onChange - handles may cross during drag

  • Memoize getRangerElement callback to prevent unnecessary re-renders

  • Use semantic HTML - render handles as <button> elements for accessibility

  • Add aria-label to describe each handle's purpose

  • Use CSS transforms (translateX ) for positioning instead of left for better performance

  • Validate in onChange to enforce constraints (min gap, max range, etc.)

  • Use onDrag for real-time feedback during drag operations

  • Consider touch targets - make handles at least 44x44px on mobile

Common Pitfalls

  • Forgetting position: relative on the track container

  • Using values instead of sortedValues (handles can swap positions)

  • Not providing getRangerElement as a callback

  • Setting thumb position with left instead of transform: translateX()

  • Forgetting to handle keyboard navigation (built-in via getHandleProps)

  • Not accounting for thumb width when calculating positions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-table

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-router

No summary provided by upstream source.

Repository SourceNeeds Review