TanStack Ranger

repository·main·Indexed 21 days ago

https://github.com/tanstack/ranger

A lightweight, typesafe headless UI library for building range components and sliders in TypeScript/JavaScript and React. It manages the complex logic of range selection, including support for single or multiple handles, custom steps, ticks, and non-linear interpolation, while leaving the visual implementation and styling entirely to the developer.

Tokens
6.2K
Snippets
21
Records
28
Agent score
74%

What's inside TanStack Ranger

  1. What is TanStack Ranger?

    main

    TanStack Ranger is a feature-rich and lightweight headless utility for building range sliders. Because it is 'headless', it does not provide or render any UI elements out of the box, allowing you to dictate your own UI and styling.

    Key characteristics include:

    • 100% Typesafe: Full TypeScript support for reliable development.
    • Lightweight: Small footprint (~10kb).
    • Extensible: Designed to be easily customized and extended.
    • UI Agnostic: Does not dictate how your components should look or be structured.
  2. Key features of TanStack Ranger

    main

    TanStack Ranger provides several core capabilities for building range inputs:

    • Headless UI: Complete control over your component's styling and structure.
    • Single or Multiple Handles: Support for both single-value sliders and multi-handle range pickers.
    • Handle Divider Items: Ability to include items between handles.
    • Custom Steps or Step-Size: Control over the increments or granularity of the range selection.
    • Custom Ticks: Support for custom tick marks and labels along the range.
  3. The role of the Ranger instance

    main

    The Ranger class is the core engine of the library. It centralizes all the logic required to build a range component and interact with its state. The instance is responsible for managing:

    • Value Range: The current selection or bounds of the range.
    • Snap Interpolation: The logic for snapping values to specific points.
    • Ticks (labels) generation: The calculation and generation of tick marks and labels.
  4. Understand Ranger's headless UI architecture

    main

    Ranger is a headless utility library, meaning it does not provide pre-built UI components or markup. Instead, it manages the underlying logic and state. To build a user interface, you must use the state and callbacks provided by Ranger's hooks to render your own custom markup (e.g., sliders, inputs, or tables).

    This approach provides:

    • Separation of Concerns: Ranger handles logic while you handle the look and feel.
    • Maintenance: A smaller API surface area makes the library easier to maintain.
    • Extensibility: You are not restricted by pre-defined UI patterns and can handle unique edge cases.
  5. Use a logarithmic interpolator in React Ranger

    main

    By default, @tanstack/react-ranger uses linear interpolation to map values to the slider track. If you need a non-linear scale (such as logarithmic), you can provide a custom interpolator object in your component options. This allows the slider to represent values where the visual distance between points is not constant, which is useful for scales like decibels or frequency.

    // Example of providing a custom interpolator
    <Ranger
      // ... other props
      interpolator={{
        getPercentageForValue: (val, min, max) => {
          // Custom logic to return percentage [0, 100]
        },
        getValueForClientX: (clientX, trackDims, min, max) => {
          // Custom logic to return value from pixel coordinate
        }
      }}
    />
  6. Install TanStack Ranger

    main

    You can install TanStack Ranger using any NPM package manager. The package you install depends on your framework of choice.

    Currently, the primary supported package is for React. Other framework integrations (Solid, Vue, Svelte, and Angular) are planned for future release.

  7. Quick Start with TanStack Ranger and React

    main

    To get started with TanStack Ranger in a React application, use the useRanger hook from @tanstack/react-ranger. You need to provide a reference to a DOM element via getRangerElement and configure the range parameters such as min, max, values, and stepSize.

    To render the handles, call rangerInstance.handles(), which returns an array of handle objects containing the necessary event handlers (onKeyDownHandler, onMouseDownHandler, onTouchStart) and state (value, isActive). You can use rangerInstance.getPercentageForValue(value) to calculate the correct CSS position for each handle.

    Key configuration options for useRanger:

    • getRangerElement: A function returning the DOM element that acts as the range container.
    • values: The current array of numeric values for the handles.
    • min: The minimum possible value.
    • max: The maximum possible value.
    • stepSize: The increment size for handle movement.
    • onChange: A callback function triggered when the range changes, receiving the Ranger instance. Use instance.sortedValues to retrieve the updated values.
    import React from 'react'
    import ReactDOM from 'react-dom'
    import { useRanger, Ranger } from '@tanstack/react-ranger'
    
    function App() {
      const rangerRef = React.useRef<HTMLDivElement>(null)
      const [values, setValues] = React.useState<ReadonlyArray<number>>([
        10, 15, 50,
      ])
    
      const rangerInstance = useRanger<HTMLDivElement>({
        getRangerElement: () => rangerRef.current,
        values,
        min: 0,
        max: 100,
        stepSize: 5,
        onChange: (instance: Ranger<HTMLDivElement>) =>
          setValues(instance.sortedValues),
      })
    
      return (
        <div className="App" style={{ padding: 10 }}>
          <h1>Basic Range</h1>
          <span>Active Index: {rangerInstance.activeHandleIndex}</span>
          <br />
          <br />
          <div
            ref={rangerRef}
            style={{
              position: 'relative',
              userSelect: 'none',
              height: '4px',
              background: '#ddd',
              boxShadow: 'inset 0 1px 2px rgba(0,0,0,.6)',
              borderRadius: '2px',
            }}
          >
            {rangerInstance
              .handles()
              .map(
                (
                  {
                    value,
                    onKeyDownHandler,
                    onMouseDownHandler,
                    onTouchStart,
                    isActive,
                  },
                  i,
                ) => (
                  <button
                    key={i}
                    onKeyDown={onKeyDownHandler}
                    onMouseDown={onMouseDownHandler}
                    onTouchStart={onTouchStart}
                    role="slider"
                    aria-valuemin={rangerInstance.options.min}
                    aria-valuemax={rangerInstance.options.max}
                    aria-valuenow={value}
                    style={{
                      position: 'absolute',
                      top: '50%',
                      left: `${rangerInstance.getPercentageForValue(value)}%`,
                      zIndex: isActive ? '1' : '0',
                      transform: 'translate(-50%, -50%)',
                      width: '14px',
                      height: '14px',
                      outline: 'none',
                      borderRadius: '100%',
                      background: 'linear-gradient(to bottom, #eee 45%, #ddd 55%)',
                      border: 'solid 1px #888',
                    }}
                  />
                ),
              )}
          </div>
          <br />
          <br />
          <br />
          <pre
            style={{
              display: 'inline-block',
              textAlign: 'left',
            }}
          >
            <code>
              {JSON.stringify({
                values,
              })}
            </code>
          </pre>
        </div>
      )
    }
    
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById('root'),
    )