useSlider
Provides the behavior and accessibility implementation for a slider component representing one or more values.
install | yarn add react-aria |
---|---|
version | 3.19.0 |
usage | import {useSlider, useSliderThumb} from 'react-aria' |
API#
useSlider(
props: AriaSliderProps,
state: SliderState,
trackRef: RefObject<Element>
): SliderAria
useSliderThumb(
(opts: AriaSliderThumbOptions,
, state: SliderState
)): SliderThumbAria
Features#
The <input type="range">
HTML element can be used to build a slider, however it is
very difficult to style cross browser. useSlider
and useSliderThumb
help achieve accessible
sliders that can be styled as needed.
- Support for one or multiple thumbs
- Support for mouse, touch, and keyboard via the useMove hook
- Multi-touch support for dragging multiple thumbs or multiple sliders at once
- Pressing on the track moves the nearest thumb to that position
- Supports using the arrow keys, as well as page up/down, home, and end keys
- Support for both horizontal and vertical orientations
- Support for custom min, max, and step values with handling for rounding errors
- Support for disabling the whole slider or individual thumbs
- Prevents text selection while dragging
- Exposed to assistive technology as a
group
ofslider
elements via ARIA - Slider thumbs use hidden native input elements to support touch screen readers
- Support for labeling both the slider as a whole and individual thumbs
- Support for displaying the current thumb values using an
<output>
element - Internationalized number formatting as a percentage or value
- Support for mirroring in RTL locales
Anatomy#
Sliders consist of a track element showing the range of available values, one or more thumbs showing the current values, an optional <output> element displaying the current values textually, and a label. The thumbs can be dragged to allow a user to change their value. In addition, the track can be clicked to move the nearest thumb to that position.
useSlider hook#
useSlider
returns three sets of props that you should spread onto the appropriate element:
Name | Type | Description |
labelProps | LabelHTMLAttributes<HTMLLabelElement> | Props for the label element. |
groupProps | DOMAttributes | Props for the root element of the slider component; groups slider inputs. |
trackProps | DOMAttributes | Props for the track element. |
outputProps | OutputHTMLAttributes<HTMLOutputElement> | Props for the output element, displaying the value of the slider thumbs. |
If there is no visual label, an aria-label
or aria-labelledby
prop must be passed instead
to identify the element to screen readers.
useSliderThumb hook#
useSliderThumb
returns props that you should spread onto the appropriate elements, along with states for styling:
Name | Type | Description |
thumbProps | DOMAttributes | Props for the root thumb element; handles the dragging motion. |
inputProps | InputHTMLAttributes<HTMLInputElement> | Props for the visually hidden range input element. |
labelProps | LabelHTMLAttributes<HTMLLabelElement> | Props for the label element for this thumb (optional). |
isDragging | boolean | Whether this thumb is currently being dragged. |
isFocused | boolean | Whether the thumb is currently focused. |
isDisabled | boolean | Whether the thumb is disabled. |
If there is no visual label, an aria-label
or aria-labelledby
prop must be passed instead
to identify each thumb to screen readers.
Slider state is managed by the useSliderState
hook.
Examples#
Single thumb#
This example shows how to build a simple horizontal slider with a single thumb. In addition, it includes a label
which can be clicked to focus the slider thumb, and an <output>
element to display the current slider value as
text. This is formatted using a locale aware number formatter provided by the useNumberFormatter hook.
The <input>
element inside the thumb is used to represent the slider to assistive technology, and is hidden from view
using the VisuallyHidden component. The thumb also uses the useFocusRing hook to
display using a different color when it is keyboard focused (try tabbing to it).
import {useSliderState} from 'react-stately';
import {mergeProps, useFocusRing, useNumberFormatter, useSlider, useSliderThumb, VisuallyHidden} from 'react-aria';
function Slider(props) {
let trackRef = React.useRef(null);
let numberFormatter = useNumberFormatter(props.formatOptions);
let state = useSliderState({ ...props, numberFormatter });
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(props, state, trackRef);
return (
<div {...groupProps} className={`slider `}>
{/* Create a container for the label and output element. */}
{props.label &&
(
<div className="label-container">
<label {...labelProps}>{props.label}</label>
<output {...outputProps}>
{state.getThumbValueLabel(0)}
</output>
</div>
)}
{/* The track element holds the visible track line and the thumb. */}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb index={0} state={state} trackRef={trackRef} />
</div>
</div>
);
}
function Thumb(props) {
let { state, trackRef, index } = props;
let inputRef = React.useRef(null);
let { thumbProps, inputProps, isDragging } = useSliderThumb({
index,
trackRef,
inputRef
}, state);
let { focusProps, isFocusVisible } = useFocusRing();
return (
<div
{...thumbProps}
className={`thumb `}
>
<VisuallyHidden>
<input ref={inputRef} {...mergeProps(inputProps, focusProps)} />
</VisuallyHidden>
</div>
);
}
<Slider label="Opacity" />
import {useSliderState} from 'react-stately';
import {
mergeProps,
useFocusRing,
useNumberFormatter,
useSlider,
useSliderThumb,
VisuallyHidden
} from 'react-aria';
function Slider(props) {
let trackRef = React.useRef(null);
let numberFormatter = useNumberFormatter(
props.formatOptions
);
let state = useSliderState({ ...props, numberFormatter });
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(props, state, trackRef);
return (
<div
{...groupProps}
className={`slider `}
>
{/* Create a container for the label and output element. */}
{props.label &&
(
<div className="label-container">
<label {...labelProps}>{props.label}</label>
<output {...outputProps}>
{state.getThumbValueLabel(0)}
</output>
</div>
)}
{/* The track element holds the visible track line and the thumb. */}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb
index={0}
state={state}
trackRef={trackRef}
/>
</div>
</div>
);
}
function Thumb(props) {
let { state, trackRef, index } = props;
let inputRef = React.useRef(null);
let { thumbProps, inputProps, isDragging } =
useSliderThumb({
index,
trackRef,
inputRef
}, state);
let { focusProps, isFocusVisible } = useFocusRing();
return (
<div
{...thumbProps}
className={`thumb `}
>
<VisuallyHidden>
<input
ref={inputRef}
{...mergeProps(inputProps, focusProps)}
/>
</VisuallyHidden>
</div>
);
}
<Slider label="Opacity" />
import {useSliderState} from 'react-stately';
import {
mergeProps,
useFocusRing,
useNumberFormatter,
useSlider,
useSliderThumb,
VisuallyHidden
} from 'react-aria';
function Slider(props) {
let trackRef = React
.useRef(null);
let numberFormatter =
useNumberFormatter(
props.formatOptions
);
let state =
useSliderState({
...props,
numberFormatter
});
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(
props,
state,
trackRef
);
return (
<div
{...groupProps}
className={`slider `}
>
{/* Create a container for the label and output element. */}
{props.label &&
(
<div className="label-container">
<label
{...labelProps}
>
{props
.label}
</label>
<output
{...outputProps}
>
{state
.getThumbValueLabel(
0
)}
</output>
</div>
)}
{/* The track element holds the visible track line and the thumb. */}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb
index={0}
state={state}
trackRef={trackRef}
/>
</div>
</div>
);
}
function Thumb(props) {
let {
state,
trackRef,
index
} = props;
let inputRef = React
.useRef(null);
let {
thumbProps,
inputProps,
isDragging
} = useSliderThumb({
index,
trackRef,
inputRef
}, state);
let {
focusProps,
isFocusVisible
} = useFocusRing();
return (
<div
{...thumbProps}
className={`thumb `}
>
<VisuallyHidden>
<input
ref={inputRef}
{...mergeProps(
inputProps,
focusProps
)}
/>
</VisuallyHidden>
</div>
);
}
<Slider label="Opacity" />
Show CSS
.slider {
display: flex;
}
.slider.horizontal {
flex-direction: column;
width: 300px;
}
.slider.vertical {
height: 150px;
}
.label-container {
display: flex;
justify-content: space-between;
}
.slider.horizontal .track {
height: 30px;
width: 100%;
}
/* track line */
.track:before {
content: attr(x);
display: block;
position: absolute;
background: gray;
}
.slider.horizontal .track:before {
height: 3px;
width: 100%;
top: 50%;
transform: translateY(-50%);
}
.slider.vertical .track {
width: 30px;
height: 100%;
}
.slider.vertical .track:before {
width: 3px;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
.thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: gray;
}
.thumb.dragging {
background: dimgray;
}
.thumb.focus {
background: orange;
}
.slider.horizontal .thumb {
top: 50%;
}
.slider.vertical .thumb {
left: 50%;
}
.track.disabled {
opacity: 0.4;
}
.slider {
display: flex;
}
.slider.horizontal {
flex-direction: column;
width: 300px;
}
.slider.vertical {
height: 150px;
}
.label-container {
display: flex;
justify-content: space-between;
}
.slider.horizontal .track {
height: 30px;
width: 100%;
}
/* track line */
.track:before {
content: attr(x);
display: block;
position: absolute;
background: gray;
}
.slider.horizontal .track:before {
height: 3px;
width: 100%;
top: 50%;
transform: translateY(-50%);
}
.slider.vertical .track {
width: 30px;
height: 100%;
}
.slider.vertical .track:before {
width: 3px;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
.thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: gray;
}
.thumb.dragging {
background: dimgray;
}
.thumb.focus {
background: orange;
}
.slider.horizontal .thumb {
top: 50%;
}
.slider.vertical .thumb {
left: 50%;
}
.track.disabled {
opacity: 0.4;
}
.slider {
display: flex;
}
.slider.horizontal {
flex-direction: column;
width: 300px;
}
.slider.vertical {
height: 150px;
}
.label-container {
display: flex;
justify-content: space-between;
}
.slider.horizontal .track {
height: 30px;
width: 100%;
}
/* track line */
.track:before {
content: attr(x);
display: block;
position: absolute;
background: gray;
}
.slider.horizontal .track:before {
height: 3px;
width: 100%;
top: 50%;
transform: translateY(-50%);
}
.slider.vertical .track {
width: 30px;
height: 100%;
}
.slider.vertical .track:before {
width: 3px;
height: 100%;
left: 50%;
transform: translateX(-50%);
}
.thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: gray;
}
.thumb.dragging {
background: dimgray;
}
.thumb.focus {
background: orange;
}
.slider.horizontal .thumb {
top: 50%;
}
.slider.vertical .thumb {
left: 50%;
}
.track.disabled {
opacity: 0.4;
}
Multi thumb#
This example shows how to build a slider with multiple thumbs. The thumb component is the same one shown in the previous
example. The main difference in this example is that there are two <Thumb>
elements rendered with different index
props.
In addition, the <output>
element uses state.getThumbValueLabel
for each thumb to display the selected range.
function RangeSlider(props) {
let trackRef = React.useRef(null);
let numberFormatter = useNumberFormatter(props.formatOptions);
let state = useSliderState({ ...props, numberFormatter });
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(props, state, trackRef);
return (
<div {...groupProps} className={`slider `}>
{props.label &&
(
<div className="label-container">
<label {...labelProps}>{props.label}</label>
<output {...outputProps}>
{` - `}
</output>
</div>
)}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb index={0} state={state} trackRef={trackRef} />
<Thumb index={1} state={state} trackRef={trackRef} />
</div>
</div>
);
}
<RangeSlider
label="Price Range"
formatOptions={{ style: 'currency', currency: 'USD' }}
maxValue={500}
defaultValue={[100, 350]}
step={10}
/>
function RangeSlider(props) {
let trackRef = React.useRef(null);
let numberFormatter = useNumberFormatter(
props.formatOptions
);
let state = useSliderState({ ...props, numberFormatter });
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(props, state, trackRef);
return (
<div
{...groupProps}
className={`slider `}
>
{props.label &&
(
<div className="label-container">
<label {...labelProps}>{props.label}</label>
<output {...outputProps}>
{` - `}
</output>
</div>
)}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb
index={0}
state={state}
trackRef={trackRef}
/>
<Thumb
index={1}
state={state}
trackRef={trackRef}
/>
</div>
</div>
);
}
<RangeSlider
label="Price Range"
formatOptions={{ style: 'currency', currency: 'USD' }}
maxValue={500}
defaultValue={[100, 350]}
step={10}
/>
function RangeSlider(
props
) {
let trackRef = React
.useRef(null);
let numberFormatter =
useNumberFormatter(
props.formatOptions
);
let state =
useSliderState({
...props,
numberFormatter
});
let {
groupProps,
trackProps,
labelProps,
outputProps
} = useSlider(
props,
state,
trackRef
);
return (
<div
{...groupProps}
className={`slider `}
>
{props.label &&
(
<div className="label-container">
<label
{...labelProps}
>
{props
.label}
</label>
<output
{...outputProps}
>
{` - `}
</output>
</div>
)}
<div
{...trackProps}
ref={trackRef}
className={`track `}
>
<Thumb
index={0}
state={state}
trackRef={trackRef}
/>
<Thumb
index={1}
state={state}
trackRef={trackRef}
/>
</div>
</div>
);
}
<RangeSlider
label="Price Range"
formatOptions={{
style: 'currency',
currency: 'USD'
}}
maxValue={500}
defaultValue={[
100,
350
]}
step={10}
/>
Usage#
The following examples show how to use the Slider
and RangeSlider
components created in the above examples.
Vertical orientation#
Sliders are horizontally oriented by default. The orientation
prop can be set to "vertical"
to create a vertical slider.
This example also uses aria-label
rather than label
to create a slider with no visible label.
<Slider
orientation="vertical"
aria-label="Opacity"
maxValue={1}
step={0.01} />
<Slider
orientation="vertical"
aria-label="Opacity"
maxValue={1}
step={0.01} />
<Slider
orientation="vertical"
aria-label="Opacity"
maxValue={1}
step={0.01} />
Controlled value#
The value
prop paired with the onChange
event can be used to make a slider controlled. The value must fall between the Slider's minimum and maximum values, which default to 0 and 100 respectively. The onChange
event receives the new slider value as a parameter, which can be used to update state.
function Example() {
let [value, setValue] = React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
value={value}
onChange={setValue} />
<p>Current value: {value}</p>
</>
);
}
function Example() {
let [value, setValue] = React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
value={value}
onChange={setValue} />
<p>Current value: {value}</p>
</>
);
}
function Example() {
let [value, setValue] =
React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
value={value}
onChange={setValue}
/>
<p>
Current value:
{' '}
{value}
</p>
</>
);
}
Multi thumb sliders specify their values as an array rather than a single number.
function Example() {
let [value, setValue] = React.useState([25, 75]);
return (
<>
<RangeSlider
label="Range"
value={value}
onChange={setValue} />
<p>Current value: {value.join(' – ')}</p>
</>
);
}
function Example() {
let [value, setValue] = React.useState([25, 75]);
return (
<>
<RangeSlider
label="Range"
value={value}
onChange={setValue} />
<p>Current value: {value.join(' – ')}</p>
</>
);
}
function Example() {
let [value, setValue] =
React.useState([
25,
75
]);
return (
<>
<RangeSlider
label="Range"
value={value}
onChange={setValue}
/>
<p>
Current value:
{' '}
{value.join(
' – '
)}
</p>
</>
);
}
onChangeEnd#
The onChangeEnd
prop can be used to handle when a user stops dragging a slider, whereas the onChange
prop is called as the user drags.
function Example() {
let [value, setValue] = React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
defaultValue={value}
onChangeEnd={setValue} />
<p>Current value: {value}</p>
</>
);
}
function Example() {
let [value, setValue] = React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
defaultValue={value}
onChangeEnd={setValue} />
<p>Current value: {value}</p>
</>
);
}
function Example() {
let [value, setValue] =
React.useState(25);
return (
<>
<Slider
label="Cookies to buy"
defaultValue={value}
onChangeEnd={setValue}
/>
<p>
Current value:
{' '}
{value}
</p>
</>
);
}
Custom value scale#
By default, slider values are precentages between 0 and 100. A different scale can be used by setting the minValue
and maxValue
props.
<Slider
label="Cookies to buy"
minValue={50}
maxValue={150}
defaultValue={100} />
<Slider
label="Cookies to buy"
minValue={50}
maxValue={150}
defaultValue={100} />
<Slider
label="Cookies to buy"
minValue={50}
maxValue={150}
defaultValue={100} />
Value formatting#
Values are formatted as a percentage by default, but this can be modified by using the formatOptions
prop to specify a different format.
formatOptions
is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale.
<Slider
label="Currency"
formatOptions={{style: 'currency', currency: 'JPY'}}
defaultValue={60} />
<Slider
label="Currency"
formatOptions={{style: 'currency', currency: 'JPY'}}
defaultValue={60} />
<Slider
label="Currency"
formatOptions={{
style: 'currency',
currency: 'JPY'
}}
defaultValue={60}
/>
Step values#
The step
prop can be used to snap the value to certain increments. The steps are calculated
starting from the minimum. For example, if minValue={2}
, and step={3}
, the valid step values would be 2, 5, 8, 11, etc.
This example allows increments of 5 between 0 and 100.
<Slider
label="Amount"
formatOptions={{style: 'currency', currency: 'USD'}}
minValue={0}
maxValue={100}
step={5} />
<Slider
label="Amount"
formatOptions={{style: 'currency', currency: 'USD'}}
minValue={0}
maxValue={100}
step={5} />
<Slider
label="Amount"
formatOptions={{
style: 'currency',
currency: 'USD'
}}
minValue={0}
maxValue={100}
step={5}
/>
Disabled#
A slider can be disabled using the isDisabled
prop.
<Slider
label="Cookies to share"
defaultValue={25}
isDisabled />
<Slider
label="Cookies to share"
defaultValue={25}
isDisabled />
<Slider
label="Cookies to share"
defaultValue={25}
isDisabled
/>
Internationalization#
Value formatting#
Formatting the value that should be displayed in the value label or aria-valuetext
is handled by useSliderState
.
The formatting can be controlled using the formatOptions
prop.
If you want to change locales, the I18nProvider
must be somewhere in the hierarchy above the Slider.
This will tell the formatter what locale to use.
RTL#
In right-to-left languages, the slider should be mirrored. The label is right-aligned, the value is left-aligned. Ensure that your CSS accounts for this.