Accessible DHTML

20th February 2004
Updated 24th January 2005

In this short article, I am going to discuss one method of creating DHTML that is accessible to a wider audience. Accessibility has many facets. In the case of DHTML there are two broad categories of users to consider. Those users that have scripting enabled browsers and those users that do not. To present a page that is accessible to the widest possible audience, it is essential to try and serve all the content for the page to that audience. This can be beneficial to both the user and the business that the page represents, as this article will demonstrate. I hope the article will elucidate what I consider an important thought process when considering the implementation of dynamic behaviour on the client side web page.

The Scenario

Take a product catalogue, where a collection of images illustrates each product, with a separate page for each product. These images are quite large, but each one displays a different and equally important feature of the product. The obvious solution is to just display all the images at the same time. But this will take up considerable screen real estate, and will impart on the chosen presentation of the catalogue (Figure 1).

Figure 1. Shows five images in the product container consuming considerable screen real estate. Compare this with Figure 2.

Figure 1. Shows five images in the product container consuming considerable screen real estate.

This is where DHTML can come to the rescue. A simple slideshow utility can be implemented, that either scrolls automatically to the next image or is controlled by the user (clicking next image - see Figure 2).

Figure 2. Shows a single image in the product container allowing room for the product description. A Next image control will cycle through the images.

Figure 2. Shows a single image in the product container allowing room for the product description. A Next Image control will cycle through the images.

However, if the slideshow is hardcoded into the page, any user that does not have the appropriate script enabled (or available) in their web browser will only see the first image. For users of alternative and assistive mediums, where images are not displayed at all, a well written description can convey considerable meaning. However, likewise, an inaccessible script may provide them with only the description of the first image, thus only conveying part of the product's features. This failure in accessibility is both detrimental to the user, and the business supplying the product and catalogue.

The Rationale

Serve the page to reach the widest possible audience before introducing any dynamic features to those clients that can receive them. This thought process is a simple one, but an important one. With modern conformance to standards DHTML has become a very powerful tool that can create impressive usability. But in the global audience, DHTML enabled clients are a subset of the total web clients. Therefore a page should never be served on the assumption that dynamic features are available.1

The Solution

In part this is the easy bit! Thanks to the power of Web Standards and the general conformance of the major browser vendors to these standards, DHTML can be created once the page has arrived at the client. The page that is served will arrive on the client with static (non-behavioural) content. If the client browser is script enabled, the page calls the external script file. This file then goes about creating the necessary presentation for dynamic features and associating corresponding behaviour. In short, dynamic behaviour is not hardcoded into the structure of the document served (e.g. <a href="#" onclick="someFunction()" onkeypress="this.onclick">).

So let us look at some of the coding to acheive this. Here is the static XHTML code that is served with regards the product images:


<head>

[..snip..]

<script type="text/javascript" src="/s/img_control.js"></script>
</head>
<body>

[..snip..]

<div id="pimgs">
<img class="pimg" id="im1" src="/p/prod/_036/002.jpg" height="180" width="240" alt="Description" />
<img class="pimg" id="im2" src="/p/prod/_036/003.jpg" height="180" width="240" alt="Description" />
<img class="pimg" id="im3" src="/p/prod/_036/001.jpg" height="180" width="240" alt="Description" />
</div>

[..snip..]

</body>

As it stands this code will reach the widest possible audience. All the images are visible on image enabled browsers. On all other user agents an informative description of the image is provided via the alt tag. So users of speech browsers, braille browsers, text browsers or just image disabled browsers can still get an informative conceptualisation of the product. If DHTML had been hardcoded, a certain portion of that global audience would lose out on the complete description2.

With the static content served, an external script file, img_control.js, (here I am using Javascript) is retrieved by script-enabled browsers. The purpose of this script is three fold:

  1. Redefine the structure of the above XHTML code - additional control elements must be added to navigate to the next and previous image.
  2. Redefine the presentation of the above XHTML code - only one image should be displayed at a time.
  3. Define behavious for the control elements.

An onload behaviour must also be defined for the document to initiate the process.

Redefine the XHTML Structure and Presentation

To access the document structure, the W3C DOM can be used 3. The initialisation script is as follows:


1  // global variable stores the total number of images in the product container
2  var gTotal;
3  // global variable stores the index of the current image displayed on the page
4  var gIndex;
5  // define a constant for the value of the tab index attribute of the navigation controls
6  var INT_TAB = 20;
7
8  function initImageControl()
9  {
10   // exit if the DOM is not supported
11   // [or use alternative DOM]
12   if (document.getElementById("pimgs"))
13   {
14     var elemC = document.getElementById("pimgs");
15     // collect all the images in the product container into an array
16     var arrImgs = elemC.getElementsByTagName("img");
17     gTotal = arrImgs.length;
18     if (gTotal > 1)
19     {
20       // redefine the presentation of all but the first image
21       for (var i = 1; i < gTotal; i++)
22       {
23           var objProp = arrImgs[i].style;    
24           objProp.display = "none";    
25           objProp.visibility = "visible";
26           objProp.height = "auto";
27           objProp.width = "auto";                    
28       }
29
30       // initialise the global variable that stores index of current image
31       // the index of the first image is 0
32       gIndex = 0;  
33
34       // create a navigation bar, append it to the product container ...
35       var elemNav = document.createElement("div");
36       elemNav.id = "navlinks";
37       elemC.appendChild(elemNav);      
38       
39       // ... and add the next and previous links
40       addLink("next");
41       addLink("previous");
42     }
43   }
44 }
45
46 function addLink(str)
47 {
48   /**
49     * creates a control element (as an anchor), sets its attributes, 
50     * assigns behaviours and appends it to the navigation element.
51     */
52   var elem = document.createElement("a");
53   var text = document.createTextNode(str);
54   elem.appendChild(text);
55   elem.setAttribute("href", "#");
56   elem.setAttribute("title", "View the " + str + " photo");
57   elem.setAttribute("tabindex", INT_TAB);
58   // add behaviour to the link
59   elem.onclick = switchImage;
60   elem.onkeypress = elem.onclick;  
61   elem.id = str.substring(0,4) + "link";
62   document.getElementById("navlinks").appendChild(elem);     
63   // on intialisation the first image is displayed, so it is 
64   // inappropriate to display the previous control link
65   if ("previous" == str) {elem.style.display = "none"};  
66 }

The above code should be pretty much self-documenting and familiar to anyone who has used the Document Object Model (in any scripting language). It creates the next and previous link (hiding the later) and redefines the presentation of all but the first image. The choice of styles in line 26 to 29 may appear a little unusual but will be explained below. (Note: setIdProperty is a user-defined utility function that sets the style for an element, identified by the value of its id property.

Define the behaviour for the control elements

The following code defines the behaviour for the next and previous links. The function switchImage is assigned to the onclick and onkeypress events of each navigation control.


1  function switchImage(evt)
2  {
3    /**
4      * simply redefines the presentation of the current image and 
5      * the image to be displayed. these are determined by the value 
6      * of the global variable gIndex.
7     */
8   evt = (evt) ? evt : (window.event) ? event : null;
9   if (evt)
10   {
11     var charCode = (evt.charCode) ? evt.charCode :
12                    ((evt.keyCode) ? evt.keyCode : null);
13     if (evt.type == "click" || charCode == 13)
14     {  
15       // the routine will only run if the event trigger was a left 
16       // mouse click or the return key pressed.
17       var elem = (evt.target) ? evt.target :
18                  ((evt.srcElement) ? evt.srcElement : null);
19       if (elem)
20       {
21         // switches the currently displayed image by hiding the image indexed by gIndex,
22         // incrementing/decrementing gIndex and making the newly indexed image visible
23         var arrImgs = document.getElementById("pimgs").getElementsByTagName("img");
24         var objProp = ("nextlink" == elem.id) 
25                     ? arrImgs[gIndex++].style
26                     : arrImgs[gIndex--].style;
27         objProp.display = "none";  
28         arrImgs[gIndex].style.display = "inline"; 
29         refreshLinks();
30         return false;
31       }
32     }
33   }
34 }
35
36 function refreshLinks()
37 {
38   /**
39     * defines the display property of the next and previous links based on
40     * the index of the current image (in the global variable gIndex).
41     */
42   document.getElementById("nextlink").style.display =
43     (gIndex == (gTotal - 1)) ? "none" : "block";
44   document.getElementById("prevlink").style.display = 
45     (gIndex == 0) ? "none" : "block";
46 }

This is a fairly simple routine, which simply redefines the image currently displayed inline, and the display status of the next and previous links. Since the behaviour has been assigned to both the onclick and onkeypress events a test must first be run to see what action triggered the event. If it was a key press event, the method should only proceed if the key pressed was return.

So by declaring the document behaviour at the end of the script file window.onload=initImages the page will be redefined to show just the first image, and a navigation bar with next link will be appended to the product container. However, there is still a rather disorientating presentational issue to resolve.

Where did those images go? Managing the CSS

The initialisation script, initImageControl hides all but the first image. Since the images must not take up screen real estate when they are not displayed, they should be declared as display:none. However, the script does not run until the page and all images have loaded, so the net result of this single declaration will be to load the images on the screen, and then remove them once they have loaded. This can be bewildering to the user, who catches a glimpse of each photo before it disappears. Therefore, the images need to be hidden while they are loading. Since this presentational rule must not interfere with the standard display rules, it must also be defined within the scripting language. The simplest way to do this is to declare the rule at the top of the script file, and pass it straight to the browser, thus simulating an inline CSS declaration.

If, however, the display property of the images is assigned the value none as inline style declarations, the images will not actually be downloaded to the cache, at least in the test browsers4. Therefore, instead they can be declared as invisible and their height and width both set to zero. The following code demonstrates this at the top of the external script file, img_control.js.


1  var strCSS = '<style type="text/css">' +
2               '/*<![CDATA[*/ ' + 
3               '#im2,#im3,#im4,#im5 {visibility:hidden;height:0;width:0;}' +
4               '/*]]>*/' +
5               '</style>'; 
6  document.write(strCSS);

Now, the presentational redefinitions during initialisation (lines 26 to 29) become clear. With the images downloaded and stored in cache, their visibility and dimensions are reset and their display set to none.

Final Words

This article has attempted to demonstrate a thought process towards the development of a more accessible DHTML implementation. If the document is served to the widest possible audience before the DHTML is built, accessibility can be achieved without the need for multiple versions. It also ensures that the client only downloads the files they need thus conserving their bandwidth. In this particular demonstration the process is beneficial to the site owner, since they are presenting their product complete to all users, whether it be as images or comprehensive alt descriptions. The code has utilised Web Standards by synthesising the W3C DOM with CSS XHTML and Javascript (ECMA Script).

As a final note, while the discussion has demonstrated a method to safely serve dynamic content to legacy browsers, it is inherently progressive, and as such XHTML 1.1 cannot be ignored. XHTML 1.1 should be served with the mime-type application/xhtml+xml. Try this in a browser that recognises it (eg Mozilla or Firefox) and the inline style declarations created by the document.write method will be ignored. Instead the style element should be created using the DOM:


1  var strCSS = '/*<![CDATA[*/ ' + 
3               '#im2,#im3,#im4,#im5 {visibility:hidden;height:0;width:0;}' +
4               '/*]]>*/'; 
5  if (document.getElementById && document.createElementNS) {
6    var elem=document.createElementNS("http://www.w3.org/1999/xhtml","style");
7    elem.setAttribute("type", "text/css");
8    var text = document.createTextNode(strCSS);
9    elem.appendChild(text);
10   document.getElementsByTagName("head")[0].appendChild(elem);
11 } else {
12   strCSS = '<style type="text/css">' + strCSS + '</style>';
13   document.write(strCSS);
14 }

To post a comment on this article please visit the corresponding web log entry.

20th February 2004, Copyright © 2004, Tom Wright, Severn Solutions

Updated on 24th January 2005

The following corrections were made -

  1. Line 56 in addLink() - str should be a reference to the variable and not part of the string literal.
  2. Line 65 in addLink() - Removed a rogue bracket from the end of the expression and wrapped in braces.
  3. Line 44 in refreshLinks() - Changed the string literal for the ID from nextlink to prevlink.

An example of the Manual Image Slideshow is available at http://css.experiments.severnsolutions.co.uk/dhtml/slideshow/.

Footnotes

  1. As an aside, it is the failure of this thought process with the serving of Flash based content that I have always found quite disheartening. The finest example being the Flash entry page that spawned the Flash Revolution. For a period these pages were popping up everywhere, typically declaring that the site requires Flash to function. Alternatively a user could view the probably not synchronised static version - I am as guilty as the rest of this Flash misuse! Flash is a great tool offering immense power for deployment of bandwidth friendly vector based animation. But it is not a solution for serving accessible content. Rather than considering the World Wide Web as a whole Flash entry pages were grabbing the evolving technology and restricting the audience of the site to a subset of the widest possible audience.
  2. For example, hiding all but the first image with CSS could make those elements unrecognisable to certain speech browsers, and would certainly make them unavailable to users with script-disabled image browsers.
  3. Of course, not all browsers implement the W3C DOM, in particular the legacy browser set. These can be accomodated by supplying multiple DOM implementations and browser detection scripts, but I am not going to discuss that here. I strongly believe in progressive development. The accessible content has been served, so users of legacy browsers (and this is a very small percentage now) are not losing out.
  4. These routines have been tested on Mozilla 1.5, Netscape 7, Opera 7, Firefox 1.8, Internet Explorer 5.x and 6 all running under Windows 98 and XP. I have also been led to believe that this works on Mac OSX (I assume using Safari).