Doing the Right Thing - How to Use HTML, CSS and JavaScript in an Accessible Manner

Christian Heilman | May 5, 2010

 

In this article we will discuss some of the misconceptions about accessible web design and show how you can build a dynamic web search without sacrificing the usability or the universal access of your site.

There are a lot of myths about the interplay of accessibility and modern web development. For years I've been battling the preconception that JavaScript is evil and cannot be used in any environment that "has to be accessible".

First of all an attitude of having to "make" something accessible is wrong. Universal access should come by design of your products and not get bolted on in a final step. This leads to seemingly accessible solutions that satisfy a legal requirement or guideline but don't help anybody out there and really are a waste of your time and money - do it right the first time, redesigns are much more expensive than new products.

Sadly enough there are a lot of people that call out for legal accessibility rather than working with developers which results, first of all, in a lot of bad examples that seemingly are the right thing to do and, secondly, in quite a lot of backlash from the scripting/development community. There are a lot of claims that thinking about accessibility holds us back in our ways of developing great web products. This is total nonsense. The web is built to make the life of people easier - regardless of ability. When you've forgotten that it is your end users you should cater for and instead try to use technology for the sake of using it, you shouldn't build for others as you are doing it wrong. We have repositories of inspirational showcases and technology portfolios - go nuts there but do not make it hard for people to use the web. It is too important as a media to mess it up.

A lot of arguments fail to understand the main concept of accessibility: it is not about catering to special cases - it is about removing barriers of entry.

Inaccessible solutions are based on a few mistakes developers make:

  • Expecting technology to be available and working
  • Expecting end users to have a certain setup and understand certain concepts without guidance
  • Using technology to please machines and not humans
  • Simulating already existing technology - and failing at creating a good replacement.

A dozen years as a professional web developer taught me one thing above all: be very paranoid about what works and about every new technology. As a market and as geeks we are hard-wired to always go for the newest, coolest and shiniest technology. The problem is that whilst we are happy to buy new gadgets all the time and upgrade our computers to the latest software that is not what all people do. In reality very few actually do the same.

The bird's eye view on accessibility

If you emancipate yourself from being in either of the two camps around the accessibility debate (developers or accessibility advocates) you will find that there are a few reasons why building accessible products are not a "nice to have" but a really clever idea:

  • Our audience is getting older every year - if I buy something online I love the convenience of it but I am hardly ever at home when it gets delivered. Older people who are less mobile don't have that problem and they should get the opportunity to buy products online.
  • Mobile computing is on the rise - instead of sitting in our homes in front of bulky desktop machines we have smartphones, laptops, notebooks, sub-notebooks and many other gadgets. All of these come with smaller screens and different means of using them - out with the mouse and keyboard, in with touch screens, rollerballs, pens and tiny, tiny keyboards.
  • People try to find and share things on the web - if a search engine can't index your content, people can't find it. If your links are not links I can send to my friends on Facebook I won't promote your content for you.
  • There are a lot of tools that help people make their own web experience - if I for example don't speak your language I can translate the page with Google Translate. If all your texts are images without alternative text I can't do that.

Which means that the process of creating accessible products is mainly coming down to a few things:

  • Build things that work regardless of input device and screen size
  • Build easy interfaces - if you can strip out a step - do it
  • Leave space in your interfaces - there is nothing more annoying than trying to tap on one link on my smartphone and accidently hitting another

HTML, CSS and JavaScript - what to do with what?

HTML, CSS and JavaScript are all technologies that run in a browser or on the client side (as the client does not have to be a browser). This means a few things:

  • They are easy to develop and debug - no need for a server to run them, just save them to the hard drive and open them up in your browser.
  • They are executed in the unknown - the browser, its configuration, the operating system, the specs of the hardware it runs on, the type of device, all of those can change constantly and there is no way for you to tell people what to use.
  • They are very lightweight by design - if you use them for heavy conversion tasks you shouldn't be surprised if things slow down.
  • They are very limited in their scope - compared to "high level" programming languages there is not much you can do with them (with JavaScript being the exception)

All of these technologies have a certain job to do:

  • HTML gives content structure and meaning - it is not there to design with it so please stop using multiple line breaks or empty paragraphs to lay out your application - this is what CSS is for. Your HTML should be the logical thing for the job - a headline is a <h1> and not a <div class="head"></div>. Your HTML should also be the right interactive element for the job - a button will execute JavaScript functionality - a link without a place to go does not by default.
  • CSS defines the look and feel - everything that is purely visual should go into the CSS. This can include hover effects and in some browsers even animation and transformations. You should however not rely on CSS for your content to make sense. Structure it in HTML that it can be read without CSS - for example on an older Blackberry device - and then make it look different using CSS.
  • JavaScript adds behavior and functionality to the static document - anything that should react to the user should be done with this technology.

In order to create clean, easy to maintain and accessible web products these layers should also stay separated which means that instead of plastering your HTML with inline event handlers like <body onload="myscript()"> you use event handling.

Enough talk, let's build this search thing

So, let's have a go at building the search form together. The first thing to think about is our HTML that we'll output. It does not matter if the HTML is going to be hard-coded or generated by JavaScript, in the end a browser and other technology on top of it will have to understand the HTML we create. For the form, let's have the following:

<form action="https://www.bing.com/search" id="bingform">
  <div>
    <label for="q">Search Bing:</label>
    <input type="text" name="q" id="q">
    <input type="submit" value="Go!" id="send">
  </div>
</form>
<div id="results"></div>

This works regardless of technology. We point the form to the Bing search result page and give it a parameter q as the search term. We use a label to connect the text Search Bing with the form field. This is very important as without the label a screen reader user would have no clue what that form field is. It also means that you can click the text to highlight the field and start editing it. This is very useful when you use checkboxes or radio buttons as they don't offer much space to click on - with a label all you have to to is click or touch the text (on a touchscreen). Labels connect via the id property, not the name. The reason is that for example in a group of radio buttons they share the same name but need to be different.

We also add a DIV that will contain the results of our search and we add some IDs to allow for access via JavaScript and styling with CSS.

And this is actually all we will write as hard-coded HTML. The reason is that you should never build HTML that needs JavaScript to make sense as when JavaScript is not available, you'll have things on the screen that don't do anything when the user activates them. This is one of the golden rules of accessible web development: do not promise things you cannot keep!

As the rest of the functionality only works when JavaScript is available, we will build it with JavaScript.

CSS the easy way

In terms of CSS and styling, we could do our own solution, but why go through the pains of testing and understanding how all kind of different browsers fail to work when you can piggy-back on a solution that's been already done for you?

In this case, I used the YUI grids CSS frameworkand especially the Grids Builder to generate the HTML framework. This has a few benefits:

  • I know it works across all kind of browsers as it is based on the YUI graded browser support and is actively being tested for me
  • The CSS is hosted by Yahoo for me on a fast, globally distributed server network
  • The grids builder adds ARIA landmarks to the document which already allows for easier screen reader access
  • Using the CSS Base module I already have a pleasing and readable font definition to play with.

In essence, I am lazy and happy to use things that work already. Using the grids all I had to add to the document to make it look like it is now was the following:

body,html{background:#999;color:#000;}
#doc{background:#fff;border:1em solid #fff;}
form{background:#ccc;padding:.5em;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;}
h2{margin:0;padding:.5em 0;color:#393;}
h3{margin:0;padding:.5em 0;color:#036;}
a:link{color:#369;}
a:visited{color:#999;}
a:hover{color:#69c;}
a:active{color:#69c;}
#related{background:#69c;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;}
#related h3{color:#000;padding:.5em;}
#related a{color:#000;}
.error, .error h2{color:#c00;}

A few colours, a few rounded corners using the CSS3 properties rather than working around all kind of flawed browsers and having to cut up images - that is all it took, welcome to 2010.

Getting access to the Bing data

Now, in order to be able to show our own search results, we need to get the data from somewhere. This is where APIs (Application Programming Interfaces) come in. Lots and lots of companies by now understand the benefit of offering their data in reusable formats and thus empowering us developers to build great stuff with them. In the case of Bing, there's the Bing developer center where I went to sign up for a developer key. I then proceeded to find the data in a format fitting for our purpose, which was JSON - and dug out the demo code for the multiple SourceType option. That made me understand how I can access the API. For example I can search the web for "Mighty Mighty Bosstones" and get the data back wrapped in a function called bing:

https://api.bing.net/json.aspx?
  AppId=B59F3913692A46D75ED39BA8F472DF267E24B611
  &Query=mighty+mighty+bosstones
  &Version=2.0
  &Sources=Web+RelatedSearch
  &JsonType=callback
  &JsonCallback=bing

All I needed now was to write the script that retrieves this data from the API. Whenever you write a solution like this, it is best to make a wish list of the functionality:

  • Users should enter a search term and be able to submit the form in any way possible - by clicking the submit button or hitting Enter.
  • To avoid confusion there should be an indicator that something is happening when the data is loading.
  • Once the data is loaded we want to show the results and the related searches in two columns and re-set the loading indicator.
  • If a user clicks any of the related searches links it should initiate a new search.

Here's all it takes - in plain JavaScript - to do that:

var bisearch = function(){
  var form,resultscontainer,send,q;
  var appid = 'B59F3913692A46D75ED39BA8F472DF267E24B611';
  function init(){
    form = document.getElementById('bingform');
    send = document.getElementById('send');
    q = document.getElementById('q');
    resultscontainer = document.getElementById('results');
    resultscontainer.addEventListener('click',bingsearch.related,false);
    if(form && resultscontainer && send && q){
      form.addEventListener('submit',bingsearch.get,false);
    }
  }

  function get(e){
    e.preventDefault();
    send.value = 'Loading...';
    var searchterm = q.value;
    var url = 'https://api.bing.net/json.aspx?'+
              'AppId=' + appid +
              '&Query=' + encodeURIComponent(searchterm) + 
              '&Version=2.0' + 
              '&Sources=Web+RelatedSearch' + 
              '&JsonType=callback' + 
              '&JsonCallback=bingsearch.success';
    var s = document.createElement('script');
    s.setAttribute('src',url);
    s.setAttribute('id','leech');
    document.getElementsByTagName('head')[0].appendChild(s);
  };

  function bingresults(o){
    send.value = 'Go!';
    var out = '';
    var old = document.getElementById('leech');
    if(old){
      old.parentNode.removeChild(old);
    }
    if(undefined === o.SearchResponse.Errors){
      var out = '<h2>Results:</h2><div class="yui-gc">' +
                '<div class="yui-u first">' +
                renderWeb(o) + 
                '</div><div class="yui-u" id="related">'+
                renderRelated(o) + 
                '</div></div>';
    } else {
      out = '<div class="error"><h2>Error</h2>' +
            '<p>' + o.SearchResponse.Errors[0].Message + '</p>' +
            '</div>';
    }
    resultscontainer.innerHTML = out;
    if(undefined !== resultscontainer.getElementsByTagName('a')[0]){
      resultscontainer.getElementsByTagName('a')[0].focus();
    }
  }

  function renderWeb(o){
    var out = '';
    var results = o.SearchResponse.Web.Results;
    if(undefined !== results){
      var all = results.length;
      out += '<h3>Web Results:</h3><ol>';
      for(var i=0;i<all;i++){
        out += '<li><h4>' +
               '<a href="' + results[i].Url + '">' + 
               results[i].Title + 
               '</a>'+
               '</h4><p>' + results[i].Description + 
               ' <span>(' + results[i].DisplayUrl + ')</span></p></li>';
      }
      out += '</ol>';
    }
    return out;
  };

  function renderRelated(o){
    var out = '';
    var related = o.SearchResponse.RelatedSearch.Results;
    if(undefined !== related){
      var all = related.length;
      out += '<h3>Related Searches:</h3><ul>';
      for(var i=0;i<all;i++){
        out += '<li>' + 
               '<a href="' + related[i].Url + '">' + 
               related[i].Title + 
               '</a>'+
               '</li>';
      }
      out += '</ul>';
    } 
    return out;
  };

  function callRelatedSearch(e){
    var t = e.target;
    var findstring = 'https://www.bing.com/search?q=';
    if(t.nodeName.toLowerCase() === 'a'){
      if(t.getAttribute('href').indexOf(findstring) !== -1){
        e.preventDefault();
        q.value = t.getAttribute('href').replace(findstring,'');
        get(e);
      }
    }
  };
  return{get:get,success:bingresults,init:init,related:callRelatedSearch}
}();
bingsearch.init();

Let's go through this code and explain what it does and why:

In order to write a clean script that does not create any global variables that might interfere with other scripts I am using the revealing module pattern of JavaScript which allows me to keep private and public variables and methods. This is a clean JavaScript code issue and doesn't have anything to do with accessibility, so let's not dwell on that. Suffice to say, this is what the whole var bisearch = function(){}(); is about.

Let's instead talk about the first part of the code:

var bisearch = function(){
  var form,resultscontainer,send,q;
  var appid = 'B59F3913692A46D75ED39BA8F472DF267E24B611';
  function init(){
    form = document.getElementById('bingform');
    send = document.getElementById('send');
    q = document.getElementById('q');
    resultscontainer = document.getElementById('results');
    if(form && resultscontainer && send && q){
            resultscontainer.addEventListener('click',bingsearch.related,false);
      form.addEventListener('submit',bingsearch.get,false);
    }
  };

We define the variables that will have to be accessible from all the methods in this solution - the form, the container for the results, the send button (that acts as a loading indicator) and the field containing the search term.

The init() method gets called immediately after the module was created by the browser and gets the ball rolling. It tests if all the necessary HTML elements exist and if they do it adds event handlers to them:

  • The form has a submit handler, that calls the get() method when the form is sent off - regardless of how the user did this. A lot of Ajax forms you see on the web use a click handler on the submit button instead but this does not work for keyboard users who just want to hit Enter wherever they are in the form - like me.
  • We assign a single click event handler to the whole results section of the page - that way we don't have to worry about the content changing dynamically. If we assigned a handler to each of the links in the related section we'd constantly have to add and remove click handlers - which is a memory nightmare. This trick is called event delegation and is terribly useful.
function get(e){
    e.preventDefault();
    send.value = 'Loading...';
    var searchterm = q.value;
    var url = 'https://api.bing.net/json.aspx?'+
              'AppId=' + appid +
              '&Query=' + encodeURIComponent(searchterm) + 
              '&Version=2.0' + 
              '&Sources=Web+RelatedSearch' + 
              '&JsonType=callback' + 
              '&JsonCallback=bingsearch.success';
    var s = document.createElement('script');
    s.setAttribute('src',url);
    s.setAttribute('id','leech');
    document.getElementsByTagName('head')[0].appendChild(s);
  };

The submit handler of the form will get the get() method. The first thing it does is call preventDefault() to make sure that the form is not being submitted and the user going to the Bing site. It then changes the text of the search button to "Loading..." to make the end user aware that there is something going on instead of expecting a problem. We assemble the URL that gets the data from the Bing API and create a new script element to get the data for us. Adding it to the head of the document gets things started. Once the API has received the data and sent back to us the function bingresults() gets called automatically - this is the beauty of JSON-P.

Notice that we are changing the value of the button to tell the user what is going on. This has a few benefits:

  • It gives the information at the spot where the interaction happened - if a user has for example zoomed into this section on a smartphone or a screen magnifier they know what the deal is rather than having to zoom out again to find a loading message somewhere on the top of the screen.
  • Changing the value of a form field dynamically triggers screen readers to refresh their copy of the site which means that we don't even leave people with severely reduced eyesight confused.
function bingresults(o){
    send.value = 'Go!';
    var out = '';
    var old = document.getElementById('leech');
    if(old){
      old.parentNode.removeChild(old);
    }

Once the data has been retrieved, we re-set the button of the form to the original state and we get rid of the dynamically created script node. That makes sure we don't end up with hundreds of script tags in the head of the document when people use this form a lot.

if(undefined === o.SearchResponse.Errors){
      var out = '<h2>Results:</h2><div class="yui-gc">' +
                '<div class="yui-u first">' +
                renderWeb(o) + 
                '</div><div class="yui-u" id="related">'+
                renderRelated(o) + 
                '</div></div>';
    } else {

If the API didn't return any errors, we can show the results. I normally tend to separate the rendering of API data and the HTML that has to go around it. This is one of those cases. The heading is followed by a YUI grids construct and both the web results and the related terms get their own rendering functions. This small set of DIVs with the right classes makes sure that our search results and the related searches are displayed next to each other in columns instead of one after the other. If you want to write the CSS to do that yourself and test it across all kind of browsers this is of course your choice to do.

Using real <h1> to <h6> headers means that they don't only display in the right fashion but they also give the document structure and help screen reader users to skip from header to header quickly.

out = '<div class="error"><h2>Error</h2>' +
            '<p>' + o.SearchResponse.Errors[0].Message + '</p>' +
            '</div>';
    }

If the API returned an error, we display that instead - wrapped in a DIV with the class error to give us a chance to style it differently. This is a good trait of the Bing API - others might return empty arrays of data instead of an error which makes our lives much harder.

resultscontainer.innerHTML = out;
    if(undefined !== resultscontainer.getElementsByTagName('a')[0]){
      resultscontainer.getElementsByTagName('a')[0].focus();
    }
  }

We write out the results to the container DIVand then send the focus of the document to the first link in the container. This has a few benefits: it informs people who can't see the screen that the loading is done and starts reading out the results and it shifts the visible part of zoomed screens or small screen devices to where the user needs to be.

function renderWeb(o){
    var out = '';
    var results = o.SearchResponse.Web.Results;
    if(undefined !== results){
      var all = results.length;
      out += '<h3>Web Results:</h3><ol>';
      for(var i=0;i<all;i++){
        out += '<li><h4>' +
               '<a href="' + results[i].Url + '">' + 
               results[i].Title + 
               '</a>'+
               '</h4><p>' + results[i].Description + 
               ' <span>(' + results[i].DisplayUrl + ')</span></p></li>';
      }
      out += '</ol>';
    }
    return out;
  };

No magic happening here, except for the structure of the HTML. Search results come in an order so an ordered list is the right construct to use. A SPAN around the display URL allows designers to do something fancy with it and real links and headers once again allow for clever keyboard navigation.

function renderRelated(o){
    var out = '';
    var related = o.SearchResponse.RelatedSearch.Results;
    if(undefined !== related){
      var all = related.length;
      out += '<h3>Related Searches:</h3><ul>';
      for(var i=0;i<all;i++){
        out += '<li>' + 
               '<a href="' + related[i].Url + '">' + 
               related[i].Title + 
               '</a>'+
               '</li>';
      }
      out += '</ul>';
    } 
    return out;
  };

For the related search terms we don't need to order the results as they are more random. Therefore a UL is sufficient.

function callRelatedSearch(e){
    var t = e.target;
    var findstring = 'https://www.bing.com/search?q=';
    if(t.nodeName.toLowerCase() === 'a'){
      if(t.getAttribute('href').indexOf(findstring) !== -1){
        e.preventDefault();
        q.value = t.getAttribute('href').replace(findstring,'');
        get(e);
      }
    }
  };

This is the event handler for the click event on the results container. Event delegation works as click events on an element happen on the element but also on all of its children. You can find out which element has been clicked by reading out the target property of the event.

What we wanted to know here is that the element that was clicked was a link and that it was one in the related terms list. The way to do that is to check that the nodeName is a (using toLowerCase() as different browsers report that differently) and that the href property contains the Bing URL https://www.bing.com/search?q. If and only if both things are satisfied we stop the normal click behaviour and instead read the search term from the link and set it as the value of the form field before calling the get() method. In essence this simulates the user entering this information and submitting the form. Automatically submitting the form with form.submit() is not advisable as it could confuse assistive technology.

return{get:get,success:bingresults,init:init,related:callRelatedSearch}
}();
bingsearch.init();

The rest is returning the methods that are accessed from the outside or from event handlers and call the init() method. And we're done - we have a dynamic search interface that is accessible and never promises more than it can deliver. Without JavaScript you go to Bing, with JavaScript you give people access to the data whilst keeping it accessible to keyboards and small screen devices.

There is not much magic to this

As you have seen a lot of this can be done by keeping things simple. Some of the interaction tricks might not have been known to you but there is nothing there that is rocket science or brain surgery. The main trick is to stop re-hashing ideas of the past and considering the use case (and its edge cases) of user interaction with your systems. Some of the tricks used here that are easy but go a very long way when it comes to making your products accessible are:

  • Use the right HTML for the job - lists, headings and links go a long way when you use them right and a good designer can make them look any way you want. They are pure magic when it comes to interaction - screen reader users can jump quickly from heading to heading and any user can click links, right click them, save them, drag them - whatever really. Browsers give us all of this for free, why people keep using SPANs to click on is beyond me.
  • Generate everything that is dependent on scripting with scripting - this covers your back in so many ways. You don't promise things to the users that don't work and frustrate them and you have a very clear indicator that something went wrong when you debug your code. It is like having a server with PHP and error reporting in the browser turned off - if nothing is shown you have a bug.
  • Give feedback where the interaction happens - changing a button when it is clicked and telling the user right there what is going on makes sure that even on a massively zoomed interface nobody gets stuck.
  • Shift the focus once you're done - guide the user to the goodies you loaded for them rather than them having to wonder where to go next.
  • Separate concerns and technologies - use event handling efficiently and keep presentation outside of the HTML and you'll not only build things that work better but are also much easier to maintain.

The last thing to keep in the back of our heads is that our world of web development is constantly evolving. If you use a solution you found on the web, check its date and do some research before blindly applying it. A lot of tricks and ideas especially in the world of accessibility have been a great idea in the past but by now are antiquated and probably even detrimental to the cause.

In my perfect world the people who know about the needs of different abilities and the ones who can build solutions to make it much easier for everyone to use the web talk a lot more. I hope that this article gives some of you an idea as to how that can happen.

Take a look a a demo of the Bing dynamic search.

 

About the Author

Christian Heilmann grew up in Germany and, after a year working for the red cross, spent a year as a radio producer. From 1997 onwards he worked for several agencies in Munich as a web developer. In 2000 he moved to the States to work for Etoys and, after the .com crash, he moved to the UK where he lead the web development department at Agilisys. In April 2006 he joined Yahoo! UK as a web developer and moved on to be the Lead Developer Evangelist for the Yahoo Developer Network. In December 2010 he moved on to Mozilla as Principal Developer Evangelist for HTML5 and the Open Web. He publishes an almost daily blog at https://wait-till-i.com and runs an article repository at https://icant.co.uk. He also authored Beginning JavaScript with DOM Scripting and Ajax: From Novice to Professional.

Find Christian on: