Custom Select Box

by JeremiePat

HTML

<select name="test" tabindex="-1">
  <option>Cherry</option>
  <option>Lemon</option>
  <option>Banana</option>
  <option>Strawberry</option>
  <option>Apple</option>
</select>
<!-- This is our main container for our widget.
     The tabindex attribute is what allow the user to focus
     out widget using the tabulation key. -->
<div class="select" tabindex="0">
  <!-- This container will be used to display the current value of the widget -->
  <span class="value">Cherry</span>
  <!-- This container will contain all the option available for our widget.
       Because it's a list, it make sens to use the ul element. -->
  <ul class="optList hidden">
    <!-- Each option only contain the value to be displayed, we'll see later 
         how to handle the real value that will be send with the form data -->
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>

CSS

body { margin : 2em; }

select {
  position: absolute;
  left: -5000em;
}

.select {
  /* This will create a positioning context for the list of option */
  position: relative;
  
  /* This will make our widget become part of the text flow and sizable at the same time */
  display : inline-block;
}

.select.active,
.select:focus {
  outline: none;
  
  /* This box-shadow property is not exactly required, however it's so important to be sure
     the active state is visibled that we use it as a default value, feel free to overide it. */
  box-shadow: 0 0 3px 1px #227755;
}

/* The .select selector here is a syntaxe sugar to be sure the classes we define are 
   the ones inside our widget. */
.select .optList {
  /* This will make sure our list of option will be display below the value 
     and out of the HTML flow */
  position: absolute;
  top     : 100%;
  left    : 0;
}

.select .optList.hidden {
  /* This is a simple way to hide the list in an accessible way, we will talk more about accessibility in the end */
  max-height: 0;
  visibility: hidden;
}

.select {
  /* All size will be express with the em value for accessibility resons 
     (to make sur the widget remain resizable if the user use its  
     browser's zoom in a text only mode). The computation are made
     assuming 1em == 16px which is the default value in almost all browsers.
     If you are lost with px to em conversion, try http://riddle.pl/emcalc/ */
  font-size   : 0.625em; /* this (10px) is the new font size context for em value in this context */
  font-family : Verdana, Arial, sans-serif;
  -moz-box-sizing : border-box;
  box-sizing : border-box;
/* We need to handle extra room for the down arrow we will add */
  padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */
  width   : 10em; /* 100px */
  border        : .2em solid #000; /* 2px */
  border-radius : .4em; /* 4px */
  box-shadow : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */
  /* The first declaration is for browsers that do not...

JavaScript

HTMLElement.prototype.hasClass = function (className) {
  var tester = new RegExp(' *\\b' + className + '\\b *', 'g');
  return this.className.search(tester) > -1;
}

HTMLElement.prototype.addClass = function (className) {
  this.className += ' ' + className;
}

HTMLElement.prototype.removeClass = function (className) {
  var tester = new RegExp(' *\\b' + className + '\\b *', 'g');
  this.className = this.className.replace(tester, ' ');
}

HTMLElement.prototype.toggleClass = function (className) {
  var func = this.hasClass(className) ? 'removeClass' : 'addClass';
  this[func](className);
}

NodeList.prototype.forEach = function (callback) {
  Array.prototype.forEach.call(this, callback);
}

window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  selectList.forEach(function (select) {
    var widget = select.previousElementSibling,
      value  = select.querySelector('.value'),
      optionList = select.querySelectorAll('.option');

    value.innerHTML = widget.value;

    optionList.forEach(function (option, index) {
      option.addEventListener('click', function (event) {
        widget.selectedIndex = index;
      }, false);

      option.addEventListener('mouseover', function (event) {
        optionList.forEach(function (option) {
          option.removeClass('highlight');
        });

        option.addClass('highlight');
      }, false);
    });

    select.addEventListener('keyup', function (event) {
      var length = widget.length,
          index  = widget.selectedIndex;

      if (event.keyCode === 40 && index < length - 1) { index++; }
      if (event.keyCode === 38 && index > 0) { index--; }

      widget.selectedIndex = index;
      value.innerHTML = widget.value;
    });

    select.addEventListener('click', function (event) {
      event.stopPropagation();
      
      this.querySelector('.optList').toggleClass('hidden');

      selectList.forEach(function (select) {
       ...