Forms & Input

Slider (Multi-Thumb)

Available

Range selection such as price ranges. Key considerations include thumb interaction constraints and labeling.

What is a multi-thumb slider?

A multi-thumb slider has two thumbs that let users select a range (lower bound to upper bound). It is commonly used for filtering by price range, age range, time range, and similar criteria.

The challenge lies in giving each thumb its own distinct label, value, and range, and preventing the lower bound from exceeding the upper bound.

Why does accessibility matter?

Live demo (recommended implementation)

Below is a two-thumb slider for selecting a price range.Each thumb is independently focusable and can be moved with the arrow keys. The minimum cannot exceed the maximum.

Accessible price range slider (two thumbs)
Price range

$2,000 – $6,000

Try it: Tab to 'Minimum price' then 'Maximum price'. Use ← → to adjust by $500, Page Up / Down by $2,000, Home / End to jump to the limits. The minimum thumb cannot cross the maximum.

Tip

When you focus a thumb, a screen reader announces something like "Minimum price, slider, $2,000" or "Maximum price, slider, $6,000," making it clear which thumb you are on and its current value. The key technique is using aria-label to distinguish between the two thumbs.

Keyboard interaction

KeyActionPriority
TabMove between thumbs (min price and max price)Required
/ Increase the focused thumb by one stepRequired
/ Decrease the focused thumb by one stepRequired
Home / EndMove the thumb to its minimum or maximum limitRequired
Page Up / Page DownIncrease or decrease by a larger stepRecommended

Required WAI-ARIA roles and properties

TargetAttribute / RoleMeaning
Each thumbrole="slider"Assign a slider role to each thumb (both thumbs if there are two).
Each thumbtabindex="0"Make each thumb individually keyboard-focusable.
Each thumbaria-label (for each)Provide a distinguishing label such as "Minimum price" or "Maximum price".
Each thumbaria-valuenowCurrent value of the thumb. Must be updated on every change.
Lower-bound thumbaria-valuemax = current value of upper boundDynamically constrain the range so the lower bound cannot exceed the upper bound.
Upper-bound thumbaria-valuemin = current value of lower boundDynamically constrain the range so the upper bound cannot go below the lower bound.
Each thumbaria-valuetextProvide a human-readable value with units such as "2000 yen" (optional but recommended).

Implementation: recommended pattern (Good)

Good / Recommended

The native range input only supports a single thumb, so a two-thumb slider is built with two elements using role="slider". The key is giving each thumb a distinct aria-label and adynamically constrained range.

Markup:

<div class="range" id="price-range">
  <span id="price-label">Price range ($)</span>

  <!-- Minimum thumb -->
  <div role="slider"
       tabindex="0"
       aria-label="Minimum price"
       aria-valuemin="0"
       aria-valuemax="6000"     <!-- Up to the max thumb's current value -->
       aria-valuenow="2000"
       aria-valuetext="$2,000"
       data-thumb="min"></div>

  <!-- Maximum thumb -->
  <div role="slider"
       tabindex="0"
       aria-label="Maximum price"
       aria-valuemin="2000"     <!-- From the min thumb's current value -->
       aria-valuemax="10000"
       aria-valuenow="6000"
       aria-valuetext="$6,000"
       data-thumb="max"></div>
</div>

Keyboard handling and the "thumbs cannot cross" constraint:

const MIN = 0, MAX = 10000, STEP = 500, BIG = 2000;
const root = document.getElementById('price-range');
const minThumb = root.querySelector('[data-thumb="min"]');
const maxThumb = root.querySelector('[data-thumb="max"]');

function val(t) { return Number(t.getAttribute('aria-valuenow')); }

function render() {
  // Clamp each thumb's range to the other's current value so they can't cross
  minThumb.setAttribute('aria-valuemin', String(MIN));
  minThumb.setAttribute('aria-valuemax', String(val(maxThumb)));
  maxThumb.setAttribute('aria-valuemin', String(val(minThumb)));
  maxThumb.setAttribute('aria-valuemax', String(MAX));

  [minThumb, maxThumb].forEach((t) => {
    t.setAttribute('aria-valuetext', '$' + val(t).toLocaleString());
    t.style.setProperty('--pos', ((val(t) - MIN) / (MAX - MIN)) * 100 + '%');
  });
}

function onKey(t) {
  return (e) => {
    const lo = Number(t.getAttribute('aria-valuemin'));
    const hi = Number(t.getAttribute('aria-valuemax'));
    const now = val(t);
    let next = now;
    switch (e.key) {
      case 'ArrowRight': case 'ArrowUp':   next = now + STEP; break;
      case 'ArrowLeft':  case 'ArrowDown': next = now - STEP; break;
      case 'PageUp':   next = now + BIG; break;
      case 'PageDown': next = now - BIG; break;
      case 'Home': next = lo; break;
      case 'End':  next = hi; break;
      default: return;
    }
    e.preventDefault();
    // Clamp to valid range so min never exceeds max
    t.setAttribute('aria-valuenow', String(Math.min(hi, Math.max(lo, next))));
    render();
  };
}

minThumb.addEventListener('keydown', onKey(minThumb));
maxThumb.addEventListener('keydown', onKey(maxThumb));
render();

Anti-pattern (Bad)

Below is a slider with two unlabeled thumbs that only respond to mouse dragging.There is no way to tell which is the lower or upper bound, and it cannot be operated with a keyboard.

Unlabeled, mouse-only two-thumb slider
Price range

Try it: You cannot Tab to the thumbs. A screen reader cannot distinguish between the two thumbs, and neither the value nor range is conveyed.

<!-- ❌ Anti-pattern: unlabeled two-thumb slider, mouse-only -->
<div class="track">
  <div class="knob" onmousedown="drag(event, this)"></div>
  <div class="knob" onmousedown="drag(event, this)"></div>
</div>
<!--
  There is no way to tell which thumb is "minimum" and which is "maximum."
  Without role / aria-valuenow, the value and range are not announced.
  Without tabindex, the thumbs cannot be reached by keyboard.
-->

Bad / Avoid

Problems with this implementation:

  • Thumbs are indistinguishable — No aria-label, so "minimum" and "maximum" cannot be told apart.
  • Not keyboard accessible — No tabindex, so neither thumb can receive focus or respond to arrow keys.
  • Value and range are not conveyed — No role="slider" or aria-valuenow.
  • No crossing constraint — The minimum can exceed the maximum without restriction, corrupting the values.

Implementation checklist


Source (English):Slider (Multi-Thumb) Pattern — W3C APG(opens in a new tab)