useSlider
Provides the behavior and accessibility implementation for a slider component representing one or more values.
| install | yarn add @react-aria/slider |
|---|---|
| version | 3.0.0-alpha.3 |
| usage | import {useSlider, useSliderThumb} from '@react-aria/slider' |
API#
useSlider(
props: SliderProps,
state: SliderState,
trackRef: RefObject<HTMLElement>
): SliderAriauseSliderThumb(
(opts: SliderThumbOptions,
, state: SliderState
)): SliderThumbAriaFeatures#
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 mouse, touch, and keyboard via the useMove hook
- Exposed to assistive technology as a
groupofsliderelements via ARIA - Slider thumbs use hidden native input elements to support touch screen readers
- Labeling support for accessibility
- Internationalized number formatting as a percentage or value
- Support for multiple thumbs, e.g. a group of range values
- Support for both horizontal and vertical orientations
- Prevents text selection while dragging
Anatomy#
Sliders consist of a track element showing the range of available values, one or more thumb 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. |
containerProps | HTMLAttributes<HTMLElement> | Props for the root element of the slider component; groups slider inputs. |
trackProps | HTMLAttributes<HTMLElement> | 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 three sets of props that you should spread onto the appropriate element:
| Name | Type | Description |
inputProps | HTMLAttributes<HTMLElement> | Props for the range input. |
thumbProps | HTMLAttributes<HTMLElement> | Props for the root thumb element; handles the dragging motion. |
labelProps | HTMLAttributes<HTMLElement> | Props for the label element for this thumb. |
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#
import {useSliderState} from '@react-stately/slider';
import {useFocusRing} from '@react-aria/focus';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {mergeProps} from '@react-aria/utils';
function Slider(props) {
let trackRef = ReactuseRef(null);
let numberFormatter = new IntlNumberFormat('en-US' propsformatOptions);
let state = useSliderState({...props numberFormatter});
let {containerProps trackProps labelProps outputProps} = useSlider(
props
state
trackRef
);
return (
<div
...containerProps
style={
position: 'relative'
display: 'flex'
flexDirection: 'column'
alignItems: 'center'
width: 300
touchAction: 'none'
}>
<div style={display: 'flex' alignSelf: 'stretch'}>
propslabel && <label ...labelProps>propslabel</label>
<output ...outputProps style={flex: '1 0 auto' textAlign: 'end'}>
stategetThumbValueLabel(0)
</output>
</div>
<div style={position: 'relative' width: '100%'}>
<div
style={
position: 'absolute'
backgroundColor: 'gray'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position: 'relative'
height: 30
width: ' 100%'
}
/>
<Thumb index=0 state=state trackRef=trackRef />
</div>
</div>
);
}
function Thumb(props) {
let {state trackRef index} = props;
let inputRef = ReactuseRef(null);
let {thumbProps inputProps} = useSliderThumb(
{
index
trackRef
inputRef
}
state
);
let {focusProps isFocusVisible} = useFocusRing();
return (
<div
style={
position: 'absolute'
top: 4
transform: 'translateX(-50%)'
left: `%`
}>
<div
...thumbProps
style={
width: 20
height: 20
borderRadius: '50%'
backgroundColor: isFocusVisible
? 'orange'
: stateisThumbDragging(index)
? 'dimgrey'
: 'gray'
}>
<VisuallyHidden>
<input ref=inputRef ...mergeProps(inputProps focusProps) />
</VisuallyHidden>
</div>
</div>
);
}
<Slider
label="Opacity"
formatOptions={style: 'percent'}
maxValue=1
step=0.01
/>import {useSliderState} from '@react-stately/slider';
import {useFocusRing} from '@react-aria/focus';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {mergeProps} from '@react-aria/utils';
function Slider(props) {
let trackRef = ReactuseRef(null);
let numberFormatter = new IntlNumberFormat(
'en-US'
propsformatOptions
);
let state = useSliderState({...props numberFormatter});
let {
containerProps
trackProps
labelProps
outputProps
} = useSlider(props state trackRef);
return (
<div
...containerProps
style={
position: 'relative'
display: 'flex'
flexDirection: 'column'
alignItems: 'center'
width: 300
touchAction: 'none'
}>
<div style={display: 'flex' alignSelf: 'stretch'}>
propslabel && (
<label ...labelProps>propslabel</label>
)
<output
...outputProps
style={flex: '1 0 auto' textAlign: 'end'}>
stategetThumbValueLabel(0)
</output>
</div>
<div style={position: 'relative' width: '100%'}>
<div
style={
position: 'absolute'
backgroundColor: 'gray'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position: 'relative'
height: 30
width: ' 100%'
}
/>
<Thumb
index=0
state=state
trackRef=trackRef
/>
</div>
</div>
);
}
function Thumb(props) {
let {state trackRef index} = props;
let inputRef = ReactuseRef(null);
let {thumbProps inputProps} = useSliderThumb(
{
index
trackRef
inputRef
}
state
);
let {focusProps isFocusVisible} = useFocusRing();
return (
<div
style={
position: 'absolute'
top: 4
transform: 'translateX(-50%)'
left: `%`
}>
<div
...thumbProps
style={
width: 20
height: 20
borderRadius: '50%'
backgroundColor: isFocusVisible
? 'orange'
: stateisThumbDragging(index)
? 'dimgrey'
: 'gray'
}>
<VisuallyHidden>
<input
ref=inputRef
...mergeProps(inputProps focusProps)
/>
</VisuallyHidden>
</div>
</div>
);
}
<Slider
label="Opacity"
formatOptions={style: 'percent'}
maxValue=1
step=0.01
/>import {useSliderState} from '@react-stately/slider';
import {useFocusRing} from '@react-aria/focus';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {mergeProps} from '@react-aria/utils';
function Slider(props) {
let trackRef = ReactuseRef(
null
);
let numberFormatter = new IntlNumberFormat(
'en-US'
propsformatOptions
);
let state = useSliderState(
{
...props
numberFormatter
}
);
let {
containerProps
trackProps
labelProps
outputProps
} = useSlider(
props
state
trackRef
);
return (
<div
...containerProps
style={
position:
'relative'
display: 'flex'
flexDirection:
'column'
alignItems:
'center'
width: 300
touchAction:
'none'
}>
<div
style={
display:
'flex'
alignSelf:
'stretch'
}>
propslabel && (
<label
...labelProps>
propslabel
</label>
)
<output
...outputProps
style={
flex:
'1 0 auto'
textAlign:
'end'
}>
stategetThumbValueLabel(
0
)
</output>
</div>
<div
style={
position:
'relative'
width: '100%'
}>
<div
style={
position:
'absolute'
backgroundColor:
'gray'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position:
'relative'
height: 30
width:
' 100%'
}
/>
<Thumb
index=0
state=state
trackRef=
trackRef
/>
</div>
</div>
);
}
function Thumb(props) {
let {
state
trackRef
index
} = props;
let inputRef = ReactuseRef(
null
);
let {
thumbProps
inputProps
} = useSliderThumb(
{
index
trackRef
inputRef
}
state
);
let {
focusProps
isFocusVisible
} = useFocusRing();
return (
<div
style={
position:
'absolute'
top: 4
transform:
'translateX(-50%)'
left: `%`
}>
<div
...thumbProps
style={
width: 20
height: 20
borderRadius:
'50%'
backgroundColor: isFocusVisible
? 'orange'
: stateisThumbDragging(
index
)
? 'dimgrey'
: 'gray'
}>
<VisuallyHidden>
<input
ref=
inputRef
...mergeProps(
inputProps
focusProps
)
/>
</VisuallyHidden>
</div>
</div>
);
}
<Slider
label="Opacity"
formatOptions={
style: 'percent'
}
maxValue=1
step=0.01
/>Multi Thumb#
function RangeSlider(props) {
let trackRef = ReactuseRef(null);
let numberFormatter = new IntlNumberFormat('en-US' propsformatOptions);
let state = useSliderState({...props numberFormatter});
let {containerProps trackProps labelProps outputProps} = useSlider(
props
state
trackRef
);
return (
<div
...containerProps
style={
position: 'relative'
display: 'flex'
flexDirection: 'column'
alignItems: 'center'
width: 300
touchAction: 'none'
}>
<div style={display: 'flex' alignSelf: 'stretch'}>
propslabel && <label ...labelProps>propslabel</label>
<output ...outputProps style={flex: '1 0 auto' textAlign: 'end'}>
` - `
</output>
</div>
<div style={position: 'relative' width: '100%'}>
<div
style={
position: 'absolute'
backgroundColor: 'grey'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position: 'relative'
height: 30
width: ' 100%'
}
/>
<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 = ReactuseRef(null);
let numberFormatter = new IntlNumberFormat(
'en-US'
propsformatOptions
);
let state = useSliderState({...props numberFormatter});
let {
containerProps
trackProps
labelProps
outputProps
} = useSlider(props state trackRef);
return (
<div
...containerProps
style={
position: 'relative'
display: 'flex'
flexDirection: 'column'
alignItems: 'center'
width: 300
touchAction: 'none'
}>
<div style={display: 'flex' alignSelf: 'stretch'}>
propslabel && (
<label ...labelProps>propslabel</label>
)
<output
...outputProps
style={flex: '1 0 auto' textAlign: 'end'}>
` - `
</output>
</div>
<div style={position: 'relative' width: '100%'}>
<div
style={
position: 'absolute'
backgroundColor: 'grey'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position: 'relative'
height: 30
width: ' 100%'
}
/>
<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 = ReactuseRef(
null
);
let numberFormatter = new IntlNumberFormat(
'en-US'
propsformatOptions
);
let state = useSliderState(
{
...props
numberFormatter
}
);
let {
containerProps
trackProps
labelProps
outputProps
} = useSlider(
props
state
trackRef
);
return (
<div
...containerProps
style={
position:
'relative'
display: 'flex'
flexDirection:
'column'
alignItems:
'center'
width: 300
touchAction:
'none'
}>
<div
style={
display:
'flex'
alignSelf:
'stretch'
}>
propslabel && (
<label
...labelProps>
propslabel
</label>
)
<output
...outputProps
style={
flex:
'1 0 auto'
textAlign:
'end'
}>
` - `
</output>
</div>
<div
style={
position:
'relative'
width: '100%'
}>
<div
style={
position:
'absolute'
backgroundColor:
'grey'
height: 3
top: 13
width: '100%'
}
/>
<div
...trackProps
ref=trackRef
style={
position:
'relative'
height: 30
width:
' 100%'
}
/>
<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
/>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.