slider
An input where the user selects a value from within a given range. Sliders allow users to select numeric values by dragging a thumb along a track or using keyboard controls.
Overview
The slider role defines an input widget that allows users to select a value from within a specified range. The user can change the value by moving the slider thumb along a track, clicking on the track, or using keyboard controls.
Sliders are commonly used for settings like volume, brightness, price ranges, and other continuous values. They provide immediate visual feedback as the value changes.
Native <input type="range"> vs role="slider"
The native <input type="range"> provides built-in keyboard support, touch interaction, and accessibility. Use role="slider" only when you need highly customized visual designs that cannot be achieved with CSS styling of the native element.
Live Demo: Slider Interactions
Volume Control
Temperature (with valuetext)
Price Range (Dual Thumbs)
Try with keyboard: Focus a slider (Tab to it), then use Arrow Keys to adjust values, Home/End for min/max, or Page Up/Down for larger steps.
Code Examples
Basic Slider
<!-- Basic Slider -->
<div class="slider-container">
<label id="volume-label">Volume</label>
<div
role="slider"
aria-labelledby="volume-label"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
tabindex="0"
>
<div class="slider-track">
<div class="slider-thumb" style="left: 50%"></div>
</div>
</div>
</div>
<!-- The slider needs:
- aria-valuenow: current value
- aria-valuemin: minimum value
- aria-valuemax: maximum value
- tabindex="0": to be focusable -->Native HTML Range (Preferred)
<!-- Native HTML Range Input (Preferred) -->
<label for="volume">Volume: <span id="volume-output">50</span>%</label>
<input
type="range"
id="volume"
min="0"
max="100"
value="50"
step="1"
oninput="document.getElementById('volume-output').textContent = this.value"
/>
<!-- Benefits of native range input:
- Built-in keyboard support
- Automatic accessibility
- Native mobile touch support
- Works without JavaScript -->
<!-- Styling native range inputs -->
<style>
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 8px;
background: #ddd;
border-radius: 4px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #4f46e5;
border-radius: 50%;
cursor: pointer;
}
</style>Keyboard Navigation
<!-- Slider with Full Keyboard Support -->
<div
role="slider"
id="brightness-slider"
aria-label="Screen brightness"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-valuetext="75 percent"
tabindex="0"
>
<div class="slider-track">
<div class="slider-fill" style="width: 75%"></div>
<div class="slider-thumb" style="left: 75%"></div>
</div>
</div>
<script>
const slider = document.getElementById('brightness-slider');
let value = 75;
const min = 0;
const max = 100;
const step = 1;
const largeStep = 10;
slider.addEventListener('keydown', (e) => {
let newValue = value;
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault();
newValue = Math.min(max, value + step);
break;
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault();
newValue = Math.max(min, value - step);
break;
case 'PageUp':
e.preventDefault();
newValue = Math.min(max, value + largeStep);
break;
case 'PageDown':
e.preventDefault();
newValue = Math.max(min, value - largeStep);
break;
case 'Home':
e.preventDefault();
newValue = min;
break;
case 'End':
e.preventDefault();
newValue = max;
break;
default:
return;
}
if (newValue !== value) {
value = newValue;
updateSlider();
}
});
function updateSlider() {
slider.setAttribute('aria-valuenow', value);
slider.setAttribute('aria-valuetext', value + ' percent');
const percent = ((value - min) / (max - min)) * 100;
slider.querySelector('.slider-fill').style.width = percent + '%';
slider.querySelector('.slider-thumb').style.left = percent + '%';
}
</script>Meaningful Value Text
<!-- Slider with Meaningful Value Text -->
<!-- Temperature slider -->
<div
role="slider"
aria-label="Thermostat"
aria-valuenow="72"
aria-valuemin="50"
aria-valuemax="90"
aria-valuetext="72 degrees Fahrenheit"
tabindex="0"
>
<!-- Visual representation -->
</div>
<!-- Price range slider -->
<div
role="slider"
aria-label="Maximum price"
aria-valuenow="150"
aria-valuemin="0"
aria-valuemax="500"
aria-valuetext="$150"
tabindex="0"
>
<!-- Visual representation -->
</div>
<!-- Rating slider -->
<div
role="slider"
aria-label="Movie rating"
aria-valuenow="4"
aria-valuemin="1"
aria-valuemax="5"
aria-valuetext="4 out of 5 stars"
tabindex="0"
>
<!-- Star icons -->
</div>Vertical Slider
<!-- Vertical Slider -->
<div class="vertical-slider-container">
<span id="volume-label">Volume</span>
<div
role="slider"
aria-labelledby="volume-label"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
aria-orientation="vertical"
tabindex="0"
>
<div class="slider-track vertical">
<div class="slider-fill" style="height: 50%"></div>
<div class="slider-thumb" style="bottom: 50%"></div>
</div>
</div>
</div>
<style>
.vertical-slider-container {
display: flex;
flex-direction: column;
align-items: center;
height: 200px;
}
.slider-track.vertical {
width: 8px;
height: 100%;
background: #ddd;
border-radius: 4px;
position: relative;
}
.slider-track.vertical .slider-fill {
position: absolute;
bottom: 0;
width: 100%;
background: #4f46e5;
border-radius: 4px;
}
.slider-track.vertical .slider-thumb {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
</style>React Component
// React Slider Component
import { useState, useRef, useCallback, useEffect } from 'react';
interface SliderProps {
label: string;
min: number;
max: number;
value: number;
step?: number;
orientation?: 'horizontal' | 'vertical';
formatValue?: (value: number) => string;
onChange: (value: number) => void;
}
function Slider({
label,
min,
max,
value,
step = 1,
orientation = 'horizontal',
formatValue = (v) => String(v),
onChange,
}: SliderProps) {
const [isDragging, setIsDragging] = useState(false);
const trackRef = useRef<HTMLDivElement>(null);
const isHorizontal = orientation === 'horizontal';
const percentage = ((value - min) / (max - min)) * 100;
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
const largeStep = step * 10;
let newValue = value;
switch (e.key) {
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault();
newValue = Math.min(max, value + step);
break;
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault();
newValue = Math.max(min, value - step);
break;
case 'PageUp':
e.preventDefault();
newValue = Math.min(max, value + largeStep);
break;
case 'PageDown':
e.preventDefault();
newValue = Math.max(min, value - largeStep);
break;
case 'Home':
e.preventDefault();
newValue = min;
break;
case 'End':
e.preventDefault();
newValue = max;
break;
default:
return;
}
onChange(newValue);
}, [value, min, max, step, onChange]);
const calculateValue = useCallback((clientX: number, clientY: number) => {
if (!trackRef.current) return value;
const rect = trackRef.current.getBoundingClientRect();
let percentage: number;
if (isHorizontal) {
percentage = (clientX - rect.left) / rect.width;
} else {
percentage = 1 - (clientY - rect.top) / rect.height;
}
percentage = Math.max(0, Math.min(1, percentage));
const newValue = min + percentage * (max - min);
return Math.round(newValue / step) * step;
}, [isHorizontal, min, max, step, value]);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
onChange(calculateValue(e.clientX, e.clientY));
}, [calculateValue, onChange]);
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
onChange(calculateValue(e.clientX, e.clientY));
};
const handleMouseUp = () => setIsDragging(false);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, calculateValue, onChange]);
return (
<div className={`slider-wrapper ${orientation}`}>
<label id="slider-label">{label}: {formatValue(value)}</label>
<div
ref={trackRef}
role="slider"
aria-labelledby="slider-label"
aria-valuenow={value}
aria-valuemin={min}
aria-valuemax={max}
aria-valuetext={formatValue(value)}
aria-orientation={orientation}
tabIndex={0}
onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
className="slider-track"
>
<div
className="slider-fill"
style={{
[isHorizontal ? 'width' : 'height']: `${percentage}%`,
}}
/>
<div
className="slider-thumb"
style={{
[isHorizontal ? 'left' : 'bottom']: `${percentage}%`,
}}
/>
</div>
</div>
);
}
// Usage
function App() {
const [volume, setVolume] = useState(50);
const [temperature, setTemperature] = useState(72);
return (
<>
<Slider
label="Volume"
min={0}
max={100}
value={volume}
formatValue={(v) => `${v}%`}
onChange={setVolume}
/>
<Slider
label="Temperature"
min={50}
max={90}
value={temperature}
formatValue={(v) => `${v}°F`}
onChange={setTemperature}
/>
</>
);
}Dual Range Slider
<!-- Dual Range Slider (Two Thumbs) -->
<div class="range-slider">
<span id="price-label">Price Range</span>
<!-- Minimum thumb -->
<div
role="slider"
aria-labelledby="price-label"
aria-label="Minimum price"
aria-valuenow="20"
aria-valuemin="0"
aria-valuemax="80"
aria-valuetext="$20"
tabindex="0"
class="slider-thumb min-thumb"
></div>
<!-- Maximum thumb -->
<div
role="slider"
aria-labelledby="price-label"
aria-label="Maximum price"
aria-valuenow="80"
aria-valuemin="20"
aria-valuemax="100"
aria-valuetext="$80"
tabindex="0"
class="slider-thumb max-thumb"
></div>
<div class="slider-track">
<div class="selected-range"></div>
</div>
</div>
<!-- Note: Each thumb is a separate slider element
The max of min-thumb should equal valuenow of max-thumb
The min of max-thumb should equal valuenow of min-thumb -->Keyboard Support
Uses the step value (default: 1)
Uses the step value (default: 1)
Typically 10x the step value
Typically 10x the step value
Jumps to aria-valuemin
Jumps to aria-valuemax
Best Practices
Use native <input type="range"> when possible for built-in accessibility
Always provide aria-valuenow, aria-valuemin, and aria-valuemax
Use aria-valuetext to provide meaningful descriptions (e.g., "$50" instead of "50")
Display the current value visually near the slider
Implement all keyboard controls (arrows, Page Up/Down, Home/End)
Make the thumb large enough to be easily grabbed (at least 44x44 CSS pixels)
Don't use sliders for selecting from a small set of discrete options - use radio buttons
Don't forget to update aria-valuenow when the value changes
Don't make the clickable area too small for touch users
Don't omit visual feedback during keyboard interaction
Supported ARIA Attributes
aria-valuenowRequiredCurrent value of the slider
aria-valueminRequiredMinimum allowed value
aria-valuemaxRequiredMaximum allowed value
aria-valuetextHuman-readable text alternative for the value
aria-orientationhorizontal (default) or vertical
aria-labelAccessible name for the slider
aria-labelledbyReferences element(s) that label the slider
aria-disabledIndicates if the slider is disabled
Common Use Cases
Accessibility Notes
Screen Reader Behavior
Screen readers announce sliders as "[label], slider, [value]" and will speak the aria-valuetext if provided, otherwise the numeric value. When the value changes, the new value is announced.
Visual Feedback
Always show the current value visually, either on the thumb, near the slider, or in a connected output element. Provide clear focus indication on the thumb for keyboard users.
Touch Accessibility
For touch devices, ensure the thumb has a large enough touch target (minimum 44x44 CSS pixels recommended). The track should also be tappable to set the value at that position.

