Provides the behavior and accessibility implementation for a slider component representing one or more values.
useSlider( props: AriaSliderProps, state: SliderState, trackRef: RefObject<HTMLElement> ): SliderAria
useSliderThumb( (opts: SliderThumbOptions, , state: SliderState )): SliderThumbAria
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.
group
of slider
elements via ARIA<output>
elementSliders 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
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 | 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
returns three sets of props that you should spread onto the appropriate element:
Name | Type | Description |
thumbProps | HTMLAttributes<HTMLElement> | 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). |
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.
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/slider'; import {useFocusRing} from '@react-aria/focus'; import {VisuallyHidden} from '@react-aria/visually-hidden'; import {mergeProps} from '@react-aria/utils'; import {useNumberFormatter} from '@react-aria/i18n'; function Slider(props) { let trackRef = ReactuseRef(null); let numberFormatter = useNumberFormatter(propsformatOptions); let state = useSliderState({...props numberFormatter}); let {groupProps trackProps labelProps outputProps} = useSlider( props state trackRef ); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' height: 3 top: 13 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'; import {useNumberFormatter} from '@react-aria/i18n'; function Slider(props) { let trackRef = ReactuseRef(null); let numberFormatter = useNumberFormatter( propsformatOptions ); let state = useSliderState({...props numberFormatter}); let { groupProps trackProps labelProps outputProps } = useSlider(props state trackRef); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' height: 3 top: 13 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'; import {useNumberFormatter} from '@react-aria/i18n'; function Slider(props) { let trackRef = ReactuseRef( null ); let numberFormatter = useNumberFormatter( propsformatOptions ); let state = useSliderState( { ...props numberFormatter } ); let { groupProps trackProps labelProps outputProps } = useSlider( props state trackRef ); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' height: 3 top: 13 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 />
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 = ReactuseRef(null); let numberFormatter = useNumberFormatter(propsformatOptions); let state = useSliderState({...props numberFormatter}); let {groupProps trackProps labelProps outputProps} = useSlider( props state trackRef ); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'grey' height: 3 top: 13 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 = useNumberFormatter( propsformatOptions ); let state = useSliderState({...props numberFormatter}); let { groupProps trackProps labelProps outputProps } = useSlider(props state trackRef); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'grey' height: 3 top: 13 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 = useNumberFormatter( propsformatOptions ); let state = useSliderState( { ...props numberFormatter } ); let { groupProps trackProps labelProps outputProps } = useSlider( props state trackRef ); return ( <div ...groupProps 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 ...trackProps ref= trackRef style={ position: 'relative' height: 30 width: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'grey' height: 3 top: 13 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 />
This example shows how to build a vertical slider. The main difference from horizontal sliders is the addition of the orientation: 'vertical'
option to both useSlider
and useSliderThumb
, and the change to the handle positioning logic. Additionally, this example shows how to build a slider without a visible label or output element. This is done by simply not using the returned labelProps
and outputProps
. Note however, that when there is no visible label, an aria-label
is required to label the slider for accessibility.
function VerticalSlider(props) { let trackRef = ReactuseRef(null); let numberFormatter = useNumberFormatter(propsformatOptions); let state = useSliderState({...props numberFormatter}); let {groupProps trackProps} = useSlider( {...props orientation: 'vertical'} state trackRef ); return ( <div ...groupProps style={height: 150 touchAction: 'none'}> <div ...trackProps ref= trackRef style={ position: 'relative' width: 30 height: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' width: 3 left: 13 height: '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( { orientation: 'vertical' index trackRef inputRef } state ); let {focusProps isFocusVisible} = useFocusRing(); return ( <div style={ position: 'absolute' left: 4 transform: 'translateY(-50%)' top: ` %`}> <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> ); } <VerticalSlider aria-label="Opacity" formatOptions={style: 'percent'} maxValue=1 step=0.01 />
function VerticalSlider(props) { let trackRef = ReactuseRef(null); let numberFormatter = useNumberFormatter( propsformatOptions ); let state = useSliderState({...props numberFormatter}); let {groupProps trackProps} = useSlider( {...props orientation: 'vertical'} state trackRef ); return ( <div ...groupProps style={height: 150 touchAction: 'none'}> <div ...trackProps ref= trackRef style={ position: 'relative' width: 30 height: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' width: 3 left: 13 height: '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( { orientation: 'vertical' index trackRef inputRef } state ); let {focusProps isFocusVisible} = useFocusRing(); return ( <div style={ position: 'absolute' left: 4 transform: 'translateY(-50%)' top: ` %`}> <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> ); } <VerticalSlider aria-label="Opacity" formatOptions={style: 'percent'} maxValue=1 step=0.01 />
function VerticalSlider( props ) { let trackRef = ReactuseRef( null ); let numberFormatter = useNumberFormatter( propsformatOptions ); let state = useSliderState( { ...props numberFormatter } ); let { groupProps trackProps } = useSlider( { ...props orientation: 'vertical' } state trackRef ); return ( <div ...groupProps style={ height: 150 touchAction: 'none' }> <div ...trackProps ref= trackRef style={ position: 'relative' width: 30 height: ' 100%' }> <div style={ position: 'absolute' backgroundColor: 'gray' width: 3 left: 13 height: '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( { orientation: 'vertical' index trackRef inputRef } state ); let { focusProps isFocusVisible } = useFocusRing(); return ( <div style={ position: 'absolute' left: 4 transform: 'translateY(-50%)' top: ` %`}> <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> ); } <VerticalSlider aria-label="Opacity" formatOptions={ style: 'percent' } maxValue=1 step=0.01 />
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.
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.
ncG1vNJzZmiqlZawtb%2FPnpqtqqWie6O4zptlnKeimnu4tc2dprCrXqOytXvRnpicrKOlsqTA0a6kaJpnbIZ6fcKemXJtkmWApLLDcpibaJWZgHiuxG6YcnFma4CjrpWecHJnlKSwtHvRnpicrF2Wv6qtjq6qnoucnrGmvo2hq6ak