Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

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.

Required Attributes
aria-valuenow, min, max
Native HTML Equivalent
<input type="range">
Keyboard Navigation
Arrows, PageUp/Down, Home/End

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

50%

Temperature (with valuetext)

72°F
50°F70°F90°F

Price Range (Dual Thumbs)

$20 - $80

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

Increases the value by one step

Uses the step value (default: 1)

Decreases the value by one step

Uses the step value (default: 1)

Page Up
Increases by a larger amount

Typically 10x the step value

Page Down
Decreases by a larger amount

Typically 10x the step value

Home
Sets to minimum value

Jumps to aria-valuemin

End
Sets to maximum value

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-valuenowRequired

Current value of the slider

aria-valueminRequired

Minimum allowed value

aria-valuemaxRequired

Maximum allowed value

aria-valuetext

Human-readable text alternative for the value

aria-orientation

horizontal (default) or vertical

aria-label

Accessible name for the slider

aria-labelledby

References element(s) that label the slider

aria-disabled

Indicates if the slider is disabled

Common Use Cases

Volume/audio controls
Brightness/contrast settings
Price range filters
Timeline/video scrubbing
Zoom level controls
Font size adjusters
Color/opacity pickers
Rating inputs (1-5 stars)
Temperature controls
Progress/timeline seekers

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.

Related Roles & Attributes

Specifications & Resources