Search

Adding Keyboard Navigation

Posted on 12th January 2010 — I was recently asked how keyboard navigation could be supported to move a slider backwards and forwards. I’ve created a few tutorials on how to create sliders and carousels but not mentioned keyboard supported navigation yet.

Watch

Watch Adding Keyboard Navigation screencast (Alternative flash version)

QuickTime version is approximately 28Mb, flash version is streaming.

View the demo used in the screencast

Important change

During the screencast I use body (though my original choice was window), but this doesn’t work in IE. So I’ve switched the code to use document.documentElement. This works across all browsers, so I’ve changed this tutorial and the demo (though the screencast uses body).

The problem

Mathieu White got in touch with me to ask how to add the keyboard support to his site. He wasn’t using my own slider code, he was using Niall Doherty’s version. Since changing the slider code was out of the question, and moving to my own carousel code to trigger the goto events would also be way too much work for a simple effect, I wanted to find a simple and easy to add solution. So to keep things as simple as possible, I worked out I should be able to layer on the functionality after the slider code was finished.

Since jQuery lends itself to finding something, then doing something to it, I would let the slider code run, then find the links on the page used to navigate the slider, and trigger them as appropriate on keyboard input.

The task is to slide the panels left or right, depending on which cursor key is pressed on the page.

The solution

As Mathieu’s slider is the primary focus of the page, we want the keyboard support to apply from anywhere on the page. And we’re only navigating left and right, which won’t interfere with scrolling up and down the page.

We need a keyboard event listener on the document.documentElement element as this is the best point for a catch all element (to work in all browsers). If an input element is focused, and you press a key, it fires the keypress event on the input element, then on all the parent elements, which eventually hits the body element, then the documentElement then the the window.

Once we have our keyboard event listener, we need to determine whether the user pressed the left or right cursor key, and if they did trigger the effect.

Triggering the effect is the trick. We’re doing this by triggering a click on the navigation links. So we’re faking a user interaction. If the user pressed the right cursor key, we need to find the navigation link that’s currently selected (in this case it has a class of current), and find the link to the right, and trigger a click on this.

Capturing key events

Then a keyboard key is pressed the event break into three separate events, which fire in the following order:

  1. keydown
  2. keypress
  3. keyup

So I’m going to use the last phase of the events and use the keyup event.

$(document.documentElement).keyup(function (event) {
  // handle cursor keys
});

Note that all event handlers in jQuery, such as click, keyup, mouseover, etc receive an event argument - which I’ve captured under the variable name event.

The left and right key codes are 37 and 39 respectively. So we only want to action the slide if these keys are pressed, so we’ll check the keyCode property on the event - this property is very well supported in browsers:

$(document.documentElement).keyup(function (event) {
  // handle cursor keys
  if (event.keyCode == 37) {
    // go left
  } else if (event.keyCode == 39) {
    // go right
  }
});

Triggering the Click on the Right Link

Triggering a click event is easy, but the problem we have to solve is finding the right link to click. Since this slider adds a class of current to the currently active link, we’ll use that as our anchor point, and try to find the link before (if the left cursor key is pressed) and after (if the right cursor key is pressed).

In some cases the current class might be on list element, which would make the navigating slightly easier, but in this case the markup looks a bit like this:

<div class="coda-slider-wrapper">
  <ul>
    <li><a class="current" href="#1">1</a></li>
    <li><a href="#2">2</a></li>
    <li><a href="#3">3</a></li>
    <li><a href="#4">4</a></li>
    <li><a href="#5">5</a></li>
  </ul>
</div>

In the case above, the “next” link is #2. Currently there’s no previous available, but we’ll let jQuery handle this for us.

To find the next link, first we need to grab the link that’s currently selected:

$('.coda-slider-wrapper ul a.current')

Next we need to move up the tree (to the li element) and move to the next element and then back down to the a element to trigger a click.

$('.coda-slider-wrapper ul a.current')
  .parent() // moves up to the li element
  .next() // moves to the adjacent li element
  .find('a') // moves down to the link
    .click(); // triggers a click on the next link

Since jQuery continues to allow chained methods to work even if there’s no elements found, we can also use prev() in the place of next() when current is on the first element and the code will work just fine still.

Let’s plug this code in to the keyboard event handler:

$(document.documentElement).keyup(function (event) {
  // handle cursor keys
  if (event.keyCode == 37) {
    // go left
    $('.coda-slider-wrapper ul a.current')
      .parent() // moves up to the li element
      .prev() // moves to the adjacent li element
      .find('a') // moves down to the link
        .click(); // triggers a click on the previous link
  } else if (event.keyCode == 39) {
    // go right
    // same as above, but just on one line and next instead of prev
    $('.coda-slider-wrapper ul a.current').parent().next().find('a').click();
  }
});

This code can be simplified since a lot of it is repetitive aside from the ‘next’ and ‘prev’. If you’re happy for slightly more complicated code, then use the code below, otherwise stick with the code above.

$(document.documentElement).keyup(function (event) {
  var direction = null;
  
  // handle cursor keys
  if (event.keyCode == 37) {
    // go left
    direction = 'prev';
  } else if (event.keyCode == 39) {
    // go right
    direction = 'next';
  }
  
  if (direction != null) {
    $('.coda-slider-wrapper ul a.current').parent()[direction]().find('a').click();
  }
});

Where does this all go?

So we have our code, but where does it all fit together with the existing code?

Since we’re going in after the slider plugin is run, so it should go directly after the plugin is initialised.

In Mathieu’s code, he had the following:

$().ready(function() {
  $('#coda-slider-1').codaSlider();
});

Note that $().ready is the same as $(document).ready (though it’s not a style I would personally use). Directly after the plugin (i.e. before the });) results in the following code:

$().ready(function() {
  $('#coda-slider-1').codaSlider();
  
  $(document.documentElement).keyup(function (event) {
    var direction = null;

    // handle cursor keys
    if (event.keyCode == 37) {
      // go left
      direction = 'prev';
    } else if (event.keyCode == 39) {
      // go right
      direction = 'next';
    }

    if (direction != null) {
      $('.coda-slider-wrapper ul a.current').parent()[direction]().find('a').click();
    }
  });
});

You can see this all in action in the demo, and be sure to use the left and right keyboard cursor keys to see the demo working.

Alternative uses

You can reuse this code for a lot of the sliders so long as there are links to navigate between the panels - you just need to navigate the DOM correctly to trigger the click on the right link based on the cursor key presses.

Related screencasts

Demo

If you find this demo doesn't work as expected, it's possibly due to the demo running from within an iframe. Try running the demo in it's own window.

Source Code

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>Keyboard Navigation</title>
<style>
body { font: 1em "Trebuchet MS", verdana, arial, sans-serif; font-size: 100%; }
input, textarea { font-family: Arial; font-size: 125%; padding: 7px; }
label { display: block; } 

.coda-slider-wrapper {margin: 18px auto; width: 482px; background: url('../images/preview-border.png') no-repeat top;}
.coda-slider { margin-bottom: 4px; }
.title {display: none;}
	
/* Use this to keep the slider content contained in a box even when JavaScript is disabled */
.coda-slider-no-js .coda-slider { height: 252px; overflow: auto !important; }
	
/* Change the width of the entire slider (without dynamic arrows) */
.coda-slider, .coda-slider .panel { width: 482px; }
	
/* Tab nav */
.coda-nav ul li a.current { background: url('../images/nav-on.png') no-repeat center; }
	
/* Panel padding */
.coda-slider .panel-wrapper {}
	
/* Preloader */
.coda-slider p.loading { padding: 20px; text-align: center }

/* Don't change anything below here unless you know what you're doing */

/* Tabbed nav */
.coda-nav ul { clear: both; display: block; overflow: hidden; width: 80px; padding: 0; margin: 0px auto; margin-bottom: 23px;}
.coda-nav ul li { display: inline-block; margin: 0; padding: 0; text-align: center;}
.coda-nav ul li a { display: block; float: left; height: 12px; width: 12px; }
	
/* Miscellaneous */
.coda-slider-wrapper { clear: both; overflow: auto }
.coda-slider { float: left; overflow: hidden; position: relative }
.coda-slider .panel { display: block; float: left }
.coda-slider .panel-container { position: relative }
.coda-nav-left, .coda-nav-right { float: left }
.coda-nav-left a, .coda-nav-right a { display: block; text-align: center; text-decoration: none; }


.coda-slider-wrapper a {
  margin: 5px;
  text-decoration: none;
  color: #999;
}

.coda-slider-wrapper a:hover {
  color: #666;
}

.coda-slider-wrapper a.current {
  color: #000;
}
</style>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="jquery.easing.1.3.js"></script>
<script src="niall-coda-slider.js"></script>

<script>
$(document).ready(function () {
  $('#coda-slider-1').codaSlider();
  
  $('body').keyup(function (event) {
    var direction = null;

    // handle cursor keys
    if (event.keyCode == 37) {
      // slide left
      direction = 'prev';
    } else if (event.keyCode == 39) {
      // slide right
      direction = 'next';
    }

    if (direction != null) {
      $('ul a.current').parent()[direction]().find('a').click();
    }
  });
});
</script>
</head>
<body>
    <h1>Keyboard Navigation</h1>
    <div class="coda-slider-wrapper">
      <div class="coda-slider preload" id="coda-slider-1">
  			<div class="panel">
  				<div class="panel-wrapper">
  					<h2 class="title">1</h2> 
  					<img src="http://farm3.static.flickr.com/2753/4203305562_f8e08b13df.jpg" />
  				</div>
  			</div>
  			<div class="panel">
  				<div class="panel-wrapper">
  					<h2 class="title">2</h2>
            <img src="http://farm3.static.flickr.com/2784/4202546683_09282da4c3.jpg" />
  				</div>
  			</div>
  			<div class="panel">
  				<div class="panel-wrapper">
  					<h2 class="title">3</h2>
            <img src="http://farm3.static.flickr.com/2739/4203305998_101049659c.jpg" />
  				</div>
  			</div>
  			<div class="panel">
  				<div class="panel-wrapper">
  					<h2 class="title">4</h2>
            <img src="http://farm5.static.flickr.com/4043/4203304632_0b0d588bb2.jpg" />
  				</div>
  			</div>
  			<div class="panel">
  				<div class="panel-wrapper">
  					<h2 class="title">5</h2>
            <img src="http://farm3.static.flickr.com/2776/4203304470_5160c7cf4c.jpg" />
  				</div>
  			</div>
  		</div>
		</div>
</body>
</html>

Comments

  1. Ben Armstrong On 12th January 2010 at 14:01

    It would be great to see the up and down arrows used to skip to the first and last items respectively as a future update. Great article though, I’ve just got to go and find somewhere to use it now…

  2. Harald Kirschner On 12th January 2010 at 14:01

    Am I doing it wrong or doesn’t work in IE 7 or 8 :-(

    Can I interest you in a MooTools-based example that works cross-browser (which looks more or less the same)?

  3. Remy On 12th January 2010 at 14:01

    @Harald - darnit, I usually test in IE but I forgot this time around - it’s been a while since the last screencast, so I was a little rusty! If you’ve got a working version in Mootools, go for it - always good to see how it’s done in other libraries.

    It turns out it was my last minute switch to using window instead of body broke it. I’ve switched the demo back to using body and it’s working in Safari + all IE now, but not in Firefox, which I’m working on now.

    …update: I’ve changed all the code to now use document.documentElement which fixes it across all browsers. That one won’t catch me out again!

  4. Harald Kirschner On 12th January 2010 at 15:01

    @Remy Hopefully it helps to understand the challenges (and the beauty of JavaScript/MooTools): http://mootools.net/shell/NRCxk/4/

    • Fantasy slider usage, imagine it ;)
    • Use keydown rather than keyup (responsiveness) and document rather than window (cross-browser)
    • Uses MooTools rather than jQuery (you can control fantasy slider instances with method calls rather than firing click events)

    For your FF problem, body needs to be 100% height to work every time, otherwise it is not always focused since it ended right after the content. document works fine for me in IE, Webkit and FF.

  5. Dan G. Switzer, II On 12th January 2010 at 15:01

    This won’t work under Firefox 3.0. You’ll need to use the keypress event instead of the keyup in order to register the arrow keys.

  6. micha On 12th January 2010 at 15:01

    You should setFocus on the first link, otherwise it won’t work out of the box.

  7. Remy On 12th January 2010 at 16:01

    @Dan G Switzer - as in Firefox 3.0 specifically or inclusive of versions above? The screencast was in 3.5 (though I had to change the code post screencast to use documentElement)

  8. Patrick H. Lauke On 12th January 2010 at 22:01

    Great stuff. Worth noting though that keyboard users (those who actually rely on it) can already use the links at the bottom (TABbing to them and activating them) - as long as they’re keyboard-focusable elements (which is why it’s important not to just use, say, spans with click behaviours etc). They’re also unlikely to want to learn new keyboard combinations that work on only one specific site. And lastly, grabbing key events for the whole document may interfere with other keyboard actions these users may take somewhere else on the page.

  9. Federico Holgado On 12th January 2010 at 23:01

    Really cool way to do this universally! I will give it a shot at some point… Thanks for the great video.

  10. Ross Bruniges On 13th January 2010 at 10:01

    So how would this work when there are multiple elements on the page that might require keyboard accessibility?

    You’ve mentioned in the article that for this use case that it’s not applicable but would have thoughts that there would be cases when it would be…fancy writing a follow up article? :)

  11. Stugoo On 13th January 2010 at 11:01

    Thanks Remy, I have a version of this working but not as tight as this. Will add it in and show you what I got.

    Stu

  12. Remy On 13th January 2010 at 16:01

    @Ross - I would say that keyboard navigation, specifically cursor key based should be used sparingly. As Patrick H. Lauke points out, navigation is obviously baked in to the browser if the element is focusable.

    The only reason cursor navigation works for this example is because it’s the majority thing on the page - i.e. you don’t go off elsewhere particularly.

    That said, if I had to do it, personally I would make the content block focusable (using tabIndex) then listening for bubbled key events on that element. Maybe worthy of another sceencast :)

  13. David McClain On 13th January 2010 at 20:01

    I’ve got a question about the re-factoring step. The direction var is a simple string so in the code instead of …parent().next().find…. you replace the .next() with direction.

    Can you explain a little about the square angle syntax?

    Awesome site, awesome demo. Explained with bags of care and attention. You can really feel the love you put into this. Great stuff!

  14. Ronny On 13th January 2010 at 20:01

    You should probably add a .stop() to it, as the event queue piles up :)

  15. Neeraj Singh On 14th January 2010 at 03:01

    Excellent screencast. I was surprised to see $('.coda-slider-wrapper ul a.current').parent()[direction].find(’a').click(); so I dig a bit deeper and posted my findings at http://www.neeraj.name/blog/articles/891-hidden-feature-of-jquery-calling-a-method-on-a-jquery-collection .

  16. sebastian On 15th January 2010 at 16:01

    Hi,

    i´m searching exactly for this to navigate trough a foto-gallery easily. But in your demo-version does not work.

    in IE6 there comes up a message in the status-bar “there are errors on this website”.

    Any ideas?

  17. Benjamin Mayo On 16th January 2010 at 19:01

    Good to see a new screencast at J4D; even better to see the same high-quality explanation I remember, even despite the time-lapse.

  18. Ben On 20th January 2010 at 00:01

    Don’t know about you guys but the demo doesn’t work on Firefox 3.5/probably needs a little tweaking.

  19. Bob On 20th January 2010 at 00:01

    To make sure the code works across all browsers that jQuery supports, you should use event.which on the event object. According to the jQuery api page on the event object, jQuery normalizes event.which to include event.keyCode and event.charCode. More info here: http://api.jquery.com/event.which/

  20. Amin On 26th January 2010 at 08:01

    That’s a wonderful work! Similar to Google web-Picasa, but I hope this JQuery code can be used as a standard.

    I wounder if we can make a code to navigate search results, instead of looking for the word “Next” or “>>”

  21. Adrian On 29th January 2010 at 11:01

    Using FF 3.5.7 on Snow Leopard it doesn’t work inside the iFrame. It does work in the “full page” version, but I still have to click on one of the links first.

    I figure the autofocus and listening for events on the block idea would solve some of that.

    Great work as always Rem!

  22. Ruana On 12th February 2010 at 13:02

    Hi, I’d love to use your keyboard navigation. Unfortunately I have a problem: in your css you define a list for the nav-tabs that’s supposed to display inline.

    the CSS for the nav-tabs:

    /* Tab nav */ .coda-nav ul li a.current { background: url(’../images/nav-on.png’) no-repeat center; }

    /* Tabbed nav */ .coda-nav ul { clear: both; display: block; overflow: hidden; width: 80px; padding: 0; margin: 0px auto; margin-bottom: 23px;} .coda-nav ul li { display: inline-block; margin: 0; padding: 0; text-align: center;} .coda-nav ul li a { display: block; float: left; height: 12px; width: 12px; }

    However, in the markup I don’t see any list whatsoever.

    HTML inside of the slider-wrapper:

    1

    Therefore the navigation displays vertically below the images. I’m good at coding (html and css) but a beginner in scripting. I was unable to make the nav-tabs display horizontally so are the tabs maybe somehow attached to the script? Or could it be a IE specific problem?

    Ruana

  23. Ruana On 12th February 2010 at 13:02

    Sorry for the double post. I just saw that the code (html) didn’t display correctly.

    It’s this part (no list that can be displayed inline-block?): div class=”panel” div class=”panel-wrapper” h2 class=”title” 1 /h2> img src=”image.jpg” / /div /div

    I also changed to body to $(document.documentElement).keyup(function (event) …

    Ruana

  24. Alex On 23rd February 2010 at 12:02

    I just found out about this site and I have to say, I love it! Keep up the good work man!

    (I’ve just started learning jQuery so this site will be a huge help for me in the long run)

  25. Armand On 1st March 2010 at 17:03

    I really like your tutorials! I just discovered this website. This might sound silly but is there any way of me getting that awesome wallpaper you have in your videos?

Comments are now closed.