Slider (Multirange)

A multi-range or multi-thumb slider is built on the same concept as a simple slider except that there are two sliding controls, to allow users to set a minimum value and a maximum value. Mobile support is a problem with multi-range sliders, so it is wise to supplement the slider with editable text fields or other similar controls that give users an alternative way to specify the range.



Search for house by price

Initial HTML Markup

<div class="deque-slider-multirange horizontal">
  <h4 class="neutral" >Search for house by price</h4>
  <span id="label"></span>
  <div>
    <label>
      min
      <input>
    </label>
    <div class="slider" style="height: 2px; width: 200px;">
      <button class="minPrice"
      role="slider"
      aria-valuemin="150000"
      aria-valuemax="450000"
      aria-orientation="horizontal"
      aria-label="Min Price"
      aria-valuenow="220000"
      data-increment="10000">
      </button>
      <button class="maxPrice"
      role="slider"
      aria-valuemin="150000"
      aria-valuemax="450000"
      aria-orientation="horizontal"
      aria-label="Max Price"
      aria-valuenow="360000"
      data-increment="10000">
      </button>
    </div>
    <label>
      max
      <input>
    </label>
  </div>
  <div id="alertRegion"></div>
</div>

JavaScript

Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js

Key parts of the JavaScript file

Note: The code below is functional only in the context of the complete JavaScript file.

In the @slider-multirange section:


	function createMultirange(slider, thumbs, minValue, maxValue, orientation) {
	  var reverse = false;
	
	  if (reverse) {
	    thumbs.forEach(function (t) {
	      return t.stepSize *= -1;
	    });
	  }
	
	  var pixels = 200;
	
	  if (orientation === null) {
	    orientation = 'horizontal';
	  }
	
	  if (isNaN(minValue) || isNaN(maxValue)) {
	    throw new Error('min, max, initial values must all be numbers. StepSize must be a number.');
	  }
	
	  if (orientation !== 'horizontal' && orientation !== 'vertical') {
	    throw new Error('orientation must be either "horizontal" or "vertical", or blank (defaults to horizontal)');
	  }
	
	  if (orientation === 'vertical') {
	    var shouldSetOrient = function shouldSetOrient() {
	      // eslint-disable-line no-inner-declarations
	      // Internet Explorer 6-11
	      var isIE = false || !!document.documentMode; //@cc_on!@
	      // Edge 20+
	      var isEdge = !isIE && !!window.StyleMedia;
	      // Firefox 1.0+
	      var isFirefox = typeof InstallTrigger !== 'undefined';
	
	      return isIE || isEdge || isFirefox;
	    };
	
	    if (shouldSetOrient() === true) {
	      slider.setAttribute('orient', 'vertical');
	    }
	  }
	
	  // the rest of this code is only relevant if there is
	  // more than one thumb.
	
	  //slider.style.height = orientation === 'horizontal' ? '2px' : pixels + 'px';
	  //slider.style.width = orientation === 'horizontal' ? pixels + 'px' : '2px';
	
	  var inputs = slider.querySelectorAll('input');
	  var minValueInput = inputs[0];
	  var maxValueInput = inputs[1];
	
	  var thumbObjects = slider.querySelectorAll('button');
	  thumbObjects = Array.prototype.slice.call(thumbObjects);
	
	  for (var i = 0; i < thumbs.length; i++) {
	    (0, _thumb.createThumbControl)(thumbObjects[i], thumbs[i], minValue, maxValue, pixels, orientation);
	  }
	
	  thumbObjects.forEach(function (t, i) {
	    if (i === 0) {
	      bindInputToThumb(minValueInput, t, minValue, maxValue);
	    } else if (i === 1) {
	      bindInputToThumb(maxValueInput, t, minValue, maxValue);
	    }
	  });
	}
	
	function bindInputToThumb(input, thumb, min, max) {
	  input.addEventListener('blur', updateThumb);
	
	  (0, _keyboardUtils.onElementEnter)(input, function (e) {
	    e.preventDefault();
	    e.stopPropagation();
	    updateThumb();
	  });
	
	  function updateThumb() {
	    var val = input.value;
	    if (thumb.textParser) {
	      val = thumb.textParser(val);
	    }
	
	    val = parseFloat(val);
	    if (!isNaN(val) && val <= max && val >= min && val != thumb.getAttribute('aria-valuenow')) {
	      thumb.setValue(val);
	    }
	  }
	
	  thumb.addEventListener('change', updateTextInput);
	
	  function updateTextInput() {
	    if (thumb.textParser) {
	      input.value = thumb.getAttribute('aria-valuetext');
	    } else {
	      input.value = thumb.getAttribute('aria-valuenow');
	    }
	  }
	
	  updateTextInput();
	}
	

In the @slider section:


	/*
	export default function createSlider(minValue = 0, maxValue = 100, initialValue = minValue, stepSize = 1, label = 'slider', orientation = 'horizontal', classes = [], labelFromValue = (v) => v) {
	  let output = document.createElement('input');
	  output.classList.add('deque');
	  output.classList.add('slider-widget');
	
	  if (isNaN(minValue) || isNaN(maxValue) || isNaN(initialValue) || isNaN(stepSize)) {
	    throw new Error('min, max, initial values must all be numbers. StepSize must be a number.');
	  }
	
	  if (minValue >= maxValue) {
	    throw new Error('minvalue must be less than maxvalue.');
	  }
	
	  if (initialValue < minValue || initialValue > maxValue) {
	    throw new Error('initial value not within bounds.');
	  }
	
	  if (orientation !== 'horizontal' && orientation !== 'vertical') {
	    throw new Error('orientation must be either "horizontal" or "vertical", or blank (defaults to horizontal)');
	  }
	
	  if (!Array.isArray(classes)) {
	    throw new Error('classes must be passed as an array.');
	  }
	
	  output.setAttribute('type', 'range');
	  output.setAttribute('min', minValue);
	  output.setAttribute('max', maxValue);
	  output.setAttribute('value', initialValue);
	  output.setAttribute('step', stepSize);
	
	  if (orientation === 'vertical') {
	    function shouldSetOrient() { // eslint-disable-line no-inner-declarations
	      // Internet Explorer 6-11
	      var isIE = false || !!document.documentMode; //@cc_on!@
	      // Edge 20+
	      var isEdge = !isIE && !!window.StyleMedia;
	      // Firefox 1.0+
	      var isFirefox = typeof InstallTrigger !== 'undefined';
	
	      return isIE || isEdge || isFirefox;
	    }
	    if(shouldSetOrient() === true){
	      output.setAttribute('orient', 'vertical');
	      output.setAttribute('aria-orientation', 'vertical');
	    } else {
	      output.setAttribute('aria-orientation', 'vertical');
	    }
	  }
	
	  classes.forEach(c => output.classList.add(c));
	
	  if (label) {
	    output.setAttribute('aria-label', label);
	  }
	
	  if (labelFromValue) {
	    output.addEventListener('change', () => {
	      output.setAttribute('aria-valuetext', 'value: ' + labelFromValue(output.value));
	    });
	  }
	
	  output.setAttribute('aria-valuetext', 'value: ' + labelFromValue(output.value));
	
	  return output;
	}
	*/
	
	function createSlider(slider, output, initialContent) {
	  var minValue = slider.getAttribute('min');
	  var initialValue = slider.getAttribute('value');
	  var maxValue = slider.getAttribute('max');
	  var stepSize = slider.getAttribute('step');
	  var orientation = slider.getAttribute('aria-orientation');
	
	  if (orientation === null) {
	    orientation = 'horizontal';
	  }
	
	  if (isNaN(minValue) || isNaN(maxValue) || isNaN(initialValue) || isNaN(stepSize)) {
	    throw new Error('min, max, initial values must all be numbers. StepSize must be a number.');
	  }
	
	  if (orientation !== 'horizontal' && orientation !== 'vertical') {
	    throw new Error('orientation must be either "horizontal" or "vertical", or blank (defaults to horizontal)');
	  }
	
	  if (orientation === 'vertical') {
	    var shouldSetOrient = function shouldSetOrient() {
	      // eslint-disable-line no-inner-declarations
	      // Internet Explorer 6-11
	      var isIE = /*@cc_on!@*/false || !!document.documentMode;
	      // Edge 20+
	      var isEdge = !isIE && !!window.StyleMedia;
	      // Firefox 1.0+
	      var isFirefox = typeof InstallTrigger !== 'undefined';
	
	      return isIE || isEdge || isFirefox;
	    };
	
	    if (shouldSetOrient() === true) {
	      slider.setAttribute('orient', 'vertical');
	    }
	  }
	
	  if (output) {
	    output.innerText = initialContent + ' ' + slider.value;
	    slider.addEventListener('change', function () {
	      slider.setAttribute('aria-valuetext', initialContent + slider.value);
	      output.innerText = initialContent + ' ' + slider.value;
	    });
	  }
	
	  slider.setAttribute('aria-valuetext', initialContent + ' ' + slider.value);
	}
	
	function activateAllSliders() {
	  var sliders = document.querySelectorAll('.deque-slider');
	  for (var i = 0; i < sliders.length; i++) {
	    var slider = sliders[i].querySelector('.deque-slider-widget');
	    var output = sliders[i].querySelector('label');
	    var initialContent = output.innerText;
	    createSlider(slider, output, initialContent);
	  }
	}
	
	activateAllSliders();
	

In the @keyboardUtils section:


	var KEYS = exports.KEYS = {
	  BACKSPACE: 8,
	  TAB: 9,
	  ENTER: 13,
	  SHIFT: 16,
	  CTRL: 17,
	  ALT: 18,
	  ESCAPE: 27,
	  SPACE: 32,
	  LEFT: 37,
	  RIGHT: 39,
	  UP: 38,
	  DOWN: 40,
	  F10: 121,
	  HOME: 36,
	  END: 35,
	  PAGE_UP: 33,
	  PAGE_DOWN: 34
	};
	
	function bindElementToEventValue(element, eventName, testValue, handler) {
	  function localHandler(e) {
	    if (e.which === testValue) {
	      handler(e);
	    }
	  }
	
	  element.addEventListener(eventName, localHandler);
	  return function () {
	    element.removeEventListener(eventName, localHandler);
	  };
	}
	
	function bindElementToKeypressValue(element, testValue, handler) {
	  return bindElementToEventValue(element, 'keypress', testValue, handler);
	}
	
	function bindElementToKeydownValue(element, testValue, handler) {
	  return bindElementToEventValue(element, 'keydown', testValue, handler);
	}
	
	function onElementEnter(element, handler) {
	  return bindElementToKeypressValue(element, KEYS.ENTER, handler);
	}
	
	function onElementEscape(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.ESCAPE, handler);
	}
	
	function onElementSpace(element, handler) {
	  return bindElementToKeypressValue(element, KEYS.SPACE, handler);
	}
	
	function onElementLeft(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.LEFT, handler);
	}
	
	function onElementRight(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.RIGHT, handler);
	}
	
	function onElementUp(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.UP, handler);
	}
	
	function onElementDown(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.DOWN, handler);
	}
	
	function onElementHome(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.HOME, handler);
	}
	
	function onElementEnd(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.END, handler);
	}
	
	function onElementPageUp(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.PAGE_UP, handler);
	}
	
	function onElementPageDown(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.PAGE_DOWN, handler);
	}
	
	function onElementF10(element, handler) {
	  return bindElementToKeydownValue(element, KEYS.F10, handler);
	}
	
	function isAlphaNumeric(charCode) {
	  return charCode >= 48 && charCode <= 90 /* numbers, uppercase letters */
	  || charCode >= 97 && charCode <= 122 /* lowercase letters */;
	}
	
	function onElementCharacter(element, handler) {
	  function localHandler(e) {
	    var charCode = e.which;
	    if (isAlphaNumeric(charCode)) {
	      handler(e);
	    }
	  }
	
	  element.addEventListener('keypress', localHandler);
	  return function () {
	    element.removeEventListener('keypress', localHandler);
	  };
	}
	
	function trapEnter(element) {
	  onElementEnter(element, function (e) {
	    e.stopPropagation();
	    e.preventDefault();
	  });
	}
	

Required: Initialization JavaScript (with functionality specific to individual pattern instances):


var formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 0,
});

function formatMoney(v) {
  return formatter.format(v);
}

function deformatMoney(v) {
  return parseFloat(v.replace(/[^0-9-.]/g, ''));
}

function removeDollarSign(v) {
  return v.replace(/(\.|\,|\$)+/g, '');
}

function showMessage(message, classes) {
  var alert = deque.createAlert(message, classes);
  alertRegion.appendChild(alert);
  document.getElementById('liveregion_assertive').append(message);
}

var multirangeSlider = document.querySelector(".deque-slider-multirange");

var startThumb = multirangeSlider.querySelector('.minPrice');
var stopThumb = multirangeSlider.querySelector('.maxPrice');

var thumbs = [{
  label: startThumb.getAttribute('aria-label'),
  labelFromValue: formatMoney,
  stepSize: startThumb.getAttribute('data-increment'),
  initialValue: startThumb.getAttribute('aria-valuenow'),
  classes: [],
  textParser: deformatMoney
},{
  label: stopThumb.getAttribute('aria-label'),
  labelFromValue: formatMoney,
  stepSize: stopThumb.getAttribute('data-increment'),
  initialValue: stopThumb.getAttribute('aria-valuenow'),
  classes: [],
  textParser: deformatMoney
}]

var minValue = startThumb.getAttribute('aria-valuemin');
minValue = parseInt(minValue);
var maxValue = startThumb.getAttribute('aria-valuemax');
maxValue = parseInt(maxValue);
var orientation = startThumb.getAttribute('aria-orientation');

deque.createMultirange(multirangeSlider, thumbs, minValue, maxValue, orientation);

var alertRegion = multirangeSlider.querySelector('#alertRegion');

var startThumb = multirangeSlider.querySelector('.minPrice');
var stopThumb = multirangeSlider.querySelector('.maxPrice');
var multirangeLabel = multirangeSlider.querySelector('#label');

// validation
var startInput = multirangeSlider.querySelector('input:first-child');
var stopInput = multirangeSlider.querySelector('label:nth-of-type(2) input');


startInput.addEventListener('blur', validateInputs);
startInput.addEventListener('keydown', onEnter);
stopInput.addEventListener('blur', validateInputs);
stopInput.addEventListener('keydown', onEnter);

function onEnter(e) {
  if(e.which === 13) {
    validateInputs(e);
  }
}

function validateInputs(e) {
  alertRegion.innerHTML = '';
  var newVal = deformatMoney(e.target.value);
  var maxVal = startThumb.getAttribute('aria-valuemax');
  var minVal = startThumb.getAttribute('aria-valuemin');
  var maxValNow = stopThumb.getAttribute('aria-valuenow');
  var minValNow = startThumb.getAttribute('aria-valuenow');

  if (newVal > maxVal || newVal < minVal) {
    e.target.classList.add('invalid');
    showMessage('Value must be between $150,000 and $450,000', ['error']);
    e.target.setAttribute('aria-invalid', true);
  } else if (isNaN(removeDollarSign(e.target.value))) {
    e.target.classList.add('invalid');
    showMessage('Please enter a valid dollar amount', ['error']);
    e.target.setAttribute('aria-invalid', true);
  } else if (minValNow > maxValNow){
    e.target.classList.add('invalid');
    showMessage('The minimum value must be less than the maximum value', ['error']);
    e.target.setAttribute('aria-invalid', true);
  } else {
    e.target.classList.remove('invalid');
    e.target.removeAttribute('aria-invalid');
  }
}

multirangeSlider.addEventListener('change', setMultirangeSliderLabel);
multirangeSlider.addEventListener('change', validateSlider);

function validateSlider(e){
  alertRegion.innerHTML = '';
  var maxValNow = stopThumb.getAttribute('aria-valuenow');
  var minValNow = startThumb.getAttribute('aria-valuenow');

  if (minValNow > maxValNow){
    e.target.classList.add('invalid');
    showMessage('The minimum value must be less than the maximum value', ['error']);
    e.target.setAttribute('aria-invalid', true);
  } else {
    e.target.classList.remove('invalid');
    e.target.removeAttribute('aria-invalid');
  }
}

function setMultirangeSliderLabel() {
  var label = 'Between ' + startThumb.getAttribute('aria-valuetext');
  label += ' and ' + stopThumb.getAttribute('aria-valuetext');

  multirangeLabel.innerText = label;
}

setMultirangeSliderLabel();

CSS

Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css

Key styles within the CSS file (other styles may also be necessary):


.deque-slider-multirange label {
  display: inline-block;
  margin: 10px;
  padding: 10px;
  font-size: 13px;
  font-family: 'Open Sans', sans-serif;
}
.deque-slider-multirange label input {
  margin: 0 0 0 10px;
  padding: 5px;
}
.deque-slider-multirange label input:focus {
  outline: 1px dashed #000000;
}
.deque-slider-multirange .slider {
  position: relative;
  height: 4px;
  background: rgba(0, 0, 0, 0.4);
  margin-top: 12px;
}
.deque-slider-multirange .slider button {
  height: 24px;
  width: 8px;
  background: #0078d7;
  padding: 0;
  border-radius: 4px;
  position: absolute;
  top: -12px;
  outline: none;
  max-width: auto;
  min-width: auto;
  border: 1px solid transparent;
}
.deque-slider-multirange .slider button:focus,
.deque-slider-multirange .slider button:active {
  outline: 1px dashed #000000;
}

Implementation Instructions

Step 1: Add Dependencies

Add deque-patterns.min.css in the <head> of the document.

<link rel="stylesheet" type="text/css" href="deque-patterns.min.css"></li>

Add a script link to deque-patterns.min.js to the bottom of the page.

<script type="text/javascript" src="deque-patterns.min.js"></script>

Step 2: Add HTML

  • Create a <div> or <span> container, with class="deque-slider-multirange horizontal". This includes the alerts and any elements required to trigger the alert.
    • Include a <h4> container with class="neutral" for sytling purposes. The inner text will be the title of the slider.
    • Add a <h3> container with id="label". This container will be filled automatically by the javascript with the inputs the user submits.
    • Create a <div> container to surround the slider pattern.
      • Create a <label> container with the inner text being "min". The javascript will add an <input> container which will update automatically when the slider controlling the minimum amount is moved and that the user can use to manually update the slider by inputing their desired amount.
      • Make the slider using a <div> container with class="slider". Also specify in the style class the height and width of the slider (e.g. stlye="height: 2px; width: 200px;").
        • Create two <button> containers with a unique class and role="slider", one for the minimum number and one for the maximum. For each include the following attributes:
        • aria-valuemin
          The lowest value your slider can represent.
          aria-valuemax
          The highest value your slider can represent.
          aria-orientation
          Your slider's orientation. Can be either 'horizontal' or 'vertical'.
          aria-label
          An accessible label for your slider.
          aria-valuenow
          The current value of this slider.
          data-increment
          The value of by how much the slider can change.
      • Create a <label> container with the inner text being "max". The javascript will add an <input> container which will update automatically when the slider controlling the maximum amount is moved and that the user can use to manually update the slider by inputing their desired amount.
    • Add a <h3> container with id="alertRegion". This container will be filled automatically by the javascript if the user makes an error.

Step 3: Add Javascript

Copy this slider-multirange.js file. As shown in the example select the container holding your slider using document.querySelector(".deque-slider-multirange") and assign it to a variable named "multirangeSlider". Then select your two buttons using thier unique class names and assign them to variables named "startThumb" and "stopThumb". The local javascript and the external file will automatically initialize the slider pattern using these variables.