tajo / react-range

🎚️Range input with a slider. Accessible. Bring your own styles and markup.
https://react-range.pages.dev
MIT License
847 stars 97 forks source link
component draggable range react slider

react-range

npm version npm downloads size stackblitz

Labeled Range

See all the other examples and their source code! Try it out in the Stackblitz sandbox!

Installation

pnpm add react-range

Usage

import * as React from "react";
import { Range } from "react-range";

const SuperSimple: React.FC = () => {
  const [values, setValues] = React.useState([50]);
  return (
    <Range
      label="Select your value"
      step={0.1}
      min={0}
      max={100}
      values={values}
      onChange={(values) => setValues(values)}
      renderTrack={({ props, children }) => (
        <div
          {...props}
          style={{
            ...props.style,
            height: "6px",
            width: "100%",
            backgroundColor: "#ccc",
          }}
        >
          {children}
        </div>
      )}
      renderThumb={({ props }) => (
        <div
          {...props}
          key={props.key}
          style={{
            ...props.style,
            height: "42px",
            width: "42px",
            backgroundColor: "#999",
          }}
        />
      )}
    />
  );
};

Features

A11y (accessibility)

Keyboard support

Assistive technologies (for example screen readers)

You are responsible for the accessibility name!

Default accessibility name is Accessibility label, set with code: aria-label="Accessibility label", but is not visible (only for screen-readers and other assistive tech), so make sure to use correct name by passing it to the prop called label.

If you want to have a visible label (best practice), then use labelledBy.

You naming options are:

Please check Basic and Basic visible label examples for more info.

Aria used on the component is following Accessible Rich Internet Applications (WAI-ARIA) 1.2 slider role, but please be aware that different assistive technologies provide different support (especially in combination with operating systems and browsers).

Therefore please make sure to test it yourself and with people with disabilities. We can not provide prompt information about support, but are happy to add your findings to this Readme.

Conformance to WCAG and other accessibility standards

We need to do more testing to claim any conformance. We did make sure the component is operable with keyboard, that it is respecting ARIA patterns for slider role and having possibility to name the component (accessible name). You are welcome to report any accessibility related findings, we look forward to add information about user tests and support for assistive technologies.

<Range /> props

renderTrack

renderTrack: (params: {
  props: {
    style: React.CSSProperties;
    ref: React.RefObject<any>;
    onMouseDown: (e: React.MouseEvent) => void;
    onTouchStart: (e: React.TouchEvent) => void;
  };
  children: React.ReactNode;
  isDragged: boolean;
  disabled: boolean;
}) => React.ReactNode;

renderTrack prop to define your track (root) element. Your function gets four parameters and should return a React component:

The track can be a single narrow div as in the Super simple example; however, it might be better to use at least two nested divs where the outer div is much thicker and has a transparent background and the inner div is narrow, has visible background and is centered. props should be then spread over the outer bigger div. Why to do this? It's nice to keep the onMouseDown and onTouchStart targets bigger since the thumb can be moved also by clicking on the track (in a single thumb scenario).

renderThumb

renderThumb: (params: {
  props: {
    key: number;
    style: React.CSSProperties;
    tabIndex?: number;
    "aria-valuemax": number;
    "aria-valuemin": number;
    "aria-valuenow": number;
    draggable: boolean;
    role: string;
    onKeyDown: (e: React.KeyboardEvent) => void;
    onKeyUp: (e: React.KeyboardEvent) => void;
  };
  value: number;
  index: number;
  isDragged: boolean;
}) => React.ReactNode;

renderThumb prop to define your thumb. Your function gets four parameters and should return a React component:

renderMark (optional)

renderMark?: (params: {
  props: {
    key: string;
    style: React.CSSProperties;
    ref: React.RefObject<any>;
  };
  index: number;
}) => React.ReactNode;

renderMark is an optional prop so you can render an element at each step. See this example. Your function gets 2 parameters and should return a React component:

You can use any dimensions for your marks and react-range will automatically position them at the correct place.

values

values: number[];

An array of numbers. It controls the position of thumbs on the track. values.length equals to the number of rendered thumbs.

onChange

onChange: (values: number[]) => void;

Called when a thumb is moved, provides new values.

onFinalChange

onFinalChange: (values: number[]) => void;

Called when a change is finished (mouse/touch up, or keyup), provides current values. Use this event when you have to make for example ajax request with new values.

min (optional)

min: number;

The range start. Can be decimal or negative. Default is 0.

max (optional)

max: number;

The range end. Can be decimal or negative. Default is 100.

step (optional)

step: number;

The minimal distance between two values. Can be decimal. Default is 1.

allowOverlap (optional)

allowOverlap: boolean;

When there are multiple thumbs on a single track, should they be allowed to overlap? Default is false.

draggableTrack (optional)

draggableTrack: boolean;

When there are multiple thumbs on a single track, should it be possible to drag all thumbs at once? Default is false.

direction (optional)

direction: Direction;

enum Direction {
  Right = "to right",
  Left = "to left",
  Down = "to bottom",
  Up = "to top",
}

It sets the orientation (vertical vs horizontal) and the direction in which the value increases. You can get this enum by:

import { Direction } from "react-range";

Default value is Direction.Right.

disabled (optional)

disabled: boolean;

If true, it ignores all touch and mouse events and makes the component not focusable. Default is false.

rtl (optional)

rtl: boolean;

If true, the slider will be optimized for RTL layouts. Default is false.

getTrackBackground

There is an additional helper function being exported from react-range. Your track is most likely a div with some background. What if you want to achieve a nice "progress bar" effect where the part before the thumb has different color than the part after? What if you want to have the same thing even with multiple thumbs (aka differently colored segments)? You don't need to glue together multiple divs in order to do that! You can use a single div and set background: linear-gradient(...). getTrackBackground function builds this verbose linear-gradient(...) for you!

getTrackBackground: (params: {
  min: number;
  max: number;
  values: number[];
  colors: string[];
  direction?: Direction;
  rtl?: boolean;
}) => string;

min, max, values and direction should be same as for the <Range /> component. colors is a list of colors. This needs to be true:

values.length + 1 === colors.length;

That's because one thumb (one value) splits the track into two segments, so you need two colors.

Motivation

There is a native input solution:

<input type="range" />

However, it has some serious shortcomings:

There are also many React based solutions but most of them are too bloated, don't support styling through CSS in JS or have lacking performance.

react-range has two main goals:

End to end testing

This library is tightly coupled to many DOM APIs. It would be very hard to ensure 100% test coverage just with unit tests that would not involve a lot of mocking. Or we could re-architect the library to better abstract all DOM interfaces but that would mean more code and bigger footprint.

Instead of that, react-range adds thorough end to end tests powered by playwright.

All tests are automatically ran in Travis CI with headless chromium. This way, the public API is well tested, including pixel-perfect positioning. Also, the tests are pretty fast, reliable and very descriptive.

Do you want to run them in the dev mode (slows down operations, opens the browser)?

pnpm ladle serve #start the ladle server
pnpm test:e2e:dev #run the e2e tests

CI mode (ladle started on the background, quick, headless)

pnpm test:e2e

Browser support

Contributing

This is how you can spin up the dev environment:

git clone https://github.com/tajo/react-range
cd react-range
pnpm install
pnpm ladle serve

Shoutouts 🙏

Big big shoutout to Tom MacWright for donating the react-range npm handle! ❤️

Author

Vojtech Miksu 2024, miksu.cz, @vmiksu