Notes to self

Creating accessible visual select in pure JavaScript without dependencies

How to create a visually more appealing select in pure JavaScript without any framework.

Requirements

Let’s imagine we need a visual select that allows users to click on images or text tags for selecting multiple countries/hobbies/tags. We can put a classic HTML <select> tag to do the job, but we want to delight the users with a more appealing visual selecting where we directly click on the options to select or deselect them. Also we might want to provide a simple input filter if there are too many options to display. On top we want this to be a general, accessible and maintainable solution.

Solution

To solve the problem we start with general HTML that works and can be used in case the JavaScript is turned off on the page:

<html>
<body>
  <form>
    <input id="filter" type="text" placeholder="Filter..." />

    <div id="virtual_select">
    </div>

    <select id="source" multiple="multiple">
      <option value="0">chess</option>
      <option value="1">swimming</option>
      <option value="2">ping pong</option>
      <option value="3">running</option>
      <option value="4">canoying</option>
      <option value="5">surfing</option>
    </select>
  </form>
</body>
</html>

Our activities select is ready and working. Yes, HTML is not broken until we break it with JavaScript. The idea behind keeping it accessible is to hide this select with JavaScript (and not default CSS) and replacing the select management with our own code. I also put two more divs there; one for filtering the options and one where we build our own visually pleasing select.

The idea of maintainability is to build our JavaScript select as function that can be used for many other selects. The public API that we will strive for is as follows:

VisualSelect(selectId, virtualSelectId, filterId);

In terms of our example we would initialize our VisualSelect function with the div IDs above:

window.onload = function(e) {
  VisualSelect("source", "virtual_select", "filter");
}

Having our function API ready we can start with an actual coding. Let’s lay out some foundation first:

<script>
  var VisualSelect = function(selectId, virtualSelectId, filterId) {
    var select = document.getElementById(selectId);
    var virtualSelect = document.getElementById(virtualSelectId);
    var filter = document.getElementById(filterId);
    var links = [];

    initializeSelect();
    initializeFilter();

    function initializeSelect() {
    }

    function initializeFilter() {
    }

    function addSelectOption(option, element) {
    }

    function enableOrDisableOption(option, button) {
    }

    function showOrHideOptionButton() {
    }

    function clearFilterSearch() {
    }
  }
</script>

First thing you notice is that I am using var for declaration. This is because we are writing ES5 and not ES6 to avoid a compilation step but keeping compatibility for older browsers. After we prepare our variables representing the elements in the document object model we initialize our select and filter. Select initialization is about building possible options from the actual HTML select with addSelectOption. Filter initialization is about setting oninput event handling with showOrHideOptionButton.

Let’s look and each function and its implementation:

function initializeSelect() {
  for (var i = 0; i < select.options.length; i = i + 1) {
    addSelectOption(select.options[i], virtualSelect);
  }
}

In the virtual select initialization we just need to go through all <select> options. We simply go through all of them and add them with addSelectOption function:

function addSelectOption(option, element) {
  var el = document.createElement("a");
  var id = selectId + "-option-" + option.value

  el.href = "#";
  el.id = id
  el.innerText = option.text;
  el.onclick = function () {
    enableOrDisableOption(option, this);
  };

  if(option.selected) {
    el.classList.add("selected");
  }

  virtualSelect.appendChild(el);
  links.push(el);
}

Here we create our option link in our virtual select. We create the link element with document.createElement function, put the option text as the link text and add an onclick event handler that will enable or disable the real select option (function below). Even though I call this select virtual we are actually modifying the DOM to include these option links. After that we set the initial state for the option by appending selected class if the option is already selected. Once we have our option ready we append it in our virtual select div and add the link to the links array which tracks all the links present.

function enableOrDisableOption(option, button) {
  if (option.selected) {
    button.classList.remove("selected");
    option.selected = false;
  } else {
    button.classList.add("selected");
    option.selected = true;
  }

  clearFilterSearch();
}

enableOrDisableOption does a simple toggle on the selected option state. It has to change both the visual appearance of the link and the actual state of the option. Once done we clear the search filter since it’s clear the correct option was found.

Now for the filter:

function initializeFilter() {
  filter.oninput = showOrHideOptionButton;
}

The initialization is obvious. We assign showOrHideOptionButton to run whenever an input changes its value.

function showOrHideOptionButton() {
  var search = this.value.toLowerCase();

  for (var i = 0; i < links.length; i = i + 1) {
    var link = links[i];

    if (link.innerText.toLowerCase().indexOf(search) >= 0) {
      link.classList.remove("hidden");
    } else {
      link.classList.add("hidden");
    }
  }
}

In showOrHideOptionButton we asks on the input’s current value with this.value. Since this function is called on our input this points to the input element. Then we go through our links that represents the link options and simply either show them or hide them by appending a hidden class (we use a class for convenience of different styling such as blurring out the element). Notice that indexOf determines if we got a match or not.

function clearFilterSearch() {
  filter.value = "";

  for (var i = 0; i < links.length; i = i + 1) {
    var link = links[i];
    link.classList.remove("hidden");
  }
}

Finally we have a function that brings all our virtual option links back to life and resets the filter value.

Result

We have highly compatible, reusable, accessible and fast solution for our new shiny select with a little bit of ES5 and DOM manipulation.

Here is the final JS fiddle.

There are of course many other ways how to do this, for example one can have a function returning a JavaScript object so that we could manually call mount (for initializing) and unmount when needed. I also did not include images for the option links, but that’s a very simple exercise.

Update: Updated a little based on nice feedback from wizao.

Work with me

I have some availability for contract work. I can be your fractional CTO, a Ruby on Rails engineer, or consultant. Write me at strzibny@strzibny.name.

RSS