Example Start

Happy Time Pizza On-line Ordering System

Select Crust

Select Vegetables

Select Carnivore Options

Select Delivery Method

Example End

Example Description

Type: Best Practice

Simple example of a tab Panel widget which used ARIA css selectors.

Keyboard Support

The following keyboard shortcuts are implemented for this example (based on recommended shortcuts specified by the DHTML Style Guide Working Group.):

If focus is on a tab button:

  • Left / Up Arrow: Show the previous tab
  • Right / Down Arrow: Show the next tab
  • Home: Show the first tab
  • End: Show the last tab

If focus is on an element in a tab panel:

  • Control + Up Arrow/Left Arrow: Set focus on the tab button for the currently displayed tab.
  • Control + Page Up: Show the previous tab and set focus on its corresponding tab button. Shows the last tab in the panel if current tab is the first one.
  • Control + Page Down: Show the next tab and set focus on its corresponding tab button. Shows the first tab in the panel if current tab is the last one.

NOTE: Google Chrome does not propagate Control+ Page Up or Control+ Page Down to the web page when multiple tabs are open. This key combination will not function correctly in that case.

Example Markup

Browser Compatibility

HTML Source Code


<div role="application">

<h2>Happy Time Pizza On-line Ordering System</h2>

<form>
<div id="tabpanel1" class="tabpanel">

  <ul class="tablist" role="tablist">
    <li id="tab1" class="tab" aria-controls="panel1" aria-selected="true" role="tab" tabindex="0">Crust</li>
    <li id="tab2" class="tab" aria-controls="panel2" role="tab" aria-selected="false" tabindex="-1">Veggies</li>
    <li id="tab3" class="tab" aria-controls="panel3" role="tab" aria-selected="false" tabindex="-1">Carnivore</li>
    <li id="tab4" class="tab" aria-controls="panel4" role="tab" aria-selected="false" tabindex="-1">Delivery</li>
  </ul>

  <div id="panel1" class="panel" aria-labelledby="tab1" role="tabpanel">
    <h3 tabindex="0">Select Crust</h3>
    
          <ul class="controlList">
            <li><input id="p1_opt1" type="radio" name="crust" value="crust1" /><label for="p1_opt1">Deep Dish</label></li>
            <li><input id="p1_opt2" type="radio" name="crust" value="crust2" checked="checked" /><label for="p1_opt2">Thick and cheesy</label></li>
            <li><input id="p1_opt3" type="radio" name="crust" value="crust3" /><label for="p1_opt3">Thick and spicy</label></li>
            <li><input id="p1_opt4" type="radio" name="crust" value="crust4" /><label for="p1_opt4">Thin</label></li>
         </ul>
  </div>

  <div id="panel2" class="panel" aria-labelledby="tab2" role="tabpanel">
    <h3 tabindex="0">Select Vegetables</h3>  
    
         <ul class="controlList">
            <li><input id="p2_opt1" type="checkbox" name="veg" value="black olives" /><label for="p2_opt1">Black Olives</label></li>
            <li><input id="p2_opt2" type="checkbox" name="veg" value="green olives" /><label for="p2_opt2">Green Olives</label></li>
            <li><input id="p2_opt3" type="checkbox" name="veg" value="green peppers" /><label for="p2_opt3">Green Peppers</label></li>
            <li><input id="p2_opt4" type="checkbox" name="veg" value="mushrooms" /><label for="p2_opt4">Mushrooms</label></li>
            <li><input id="p2_opt5" type="checkbox" name="veg" value="onions" /><label for="p2_opt5">Onions</label></li>
            <li><input id="p2_opt6" type="checkbox" name="veg" value="pineapple" /><label for="p2_opt6">Pineapple</label></li>
         </ul>
  </div>

  <div id="panel3" class="panel" aria-labelledby="tab3" role="tabpanel">
    <h3 tabindex="0">Select Carnivore Options</h3>
    
          <ul class="controlList">
            <li><input id="p3_opt1" type="checkbox" name="meat" value="pepperoni" /><label for="p3_opt1">Pepperoni</label></li>
            <li><input id="p3_opt2" type="checkbox" name="meat" value="sausage" /><label for="p3_opt2">Italian Sausage</label></li>
            <li><input id="p3_opt3" type="checkbox" name="meat" value="ham" /><label for="p3_opt3">Ham</label></li>
            <li><input id="p3_opt4" type="checkbox" name="meat" value="hamburger" /><label for="p3_opt4">Hamburger</label></li>
          </ul>
  </div>

  <div id="panel4" class="panel" aria-labelledby="tab4" role="tabpanel">
     <h3 tabindex="0">Select Delivery Method</h3>
    
    <ul class="controlList">
      <li><input id="p4_opt1" type="radio" name="delivery" value="delivery1" checked="checked" /><label for="p4_opt1">Delivery</label></li>
      <li><input id="p4_opt2" type="radio" name="delivery" value="delivery2" /><label for="p4_opt2">Eat in</label></li>
      <li><input id="p4_opt3" type="radio" name="delivery" value="delivery3" /><label for="p4_opt3">Carry out</label></li>
      <li><input id="p4_opt4" type="radio" name="delivery" value="delivery4" /><label for="p4_opt4">Overnight mail</label></li>
    </ul>
  </div>
</div>
</form>

</div>

CSS Code


.tabpanel {
  margin: 20px;
  padding: 0;
  height: 1%; /* IE fix for float bug */
}
.tablist {
  margin: 0 0px;
  padding: 0;
  list-style: none;
}

.tab {

  margin: .2em 1px 0 0;
  padding: 10px;
  height: 1em;
  font-weight: bold;
  background-color: #ec9;

  border: 1px solid black;
  -webkit-border-radius-topright: 5px;
  -webkit-border-radius-topleft: 5px;
  -moz-border-radius-topright: 5px;
  -moz-border-radius-topleft: 5px;
  border-radius-topright: 5px;
  border-radius-topleft: 5px;

  float: left;
  display: inline; /* IE float bug fix */
}

.panel {
  clear: both;
  display: block;
  margin: 0 0 0 0;
  padding: 10px;
  width: 600px;
  border: 1px solid black;

  -webkit-border-radius-topright: 10px;
  -webkit-border-radius-bottomleft: 10px;
  -webkit-border-radius-bottomright: 10px;
  -moz-border-radius-topright: 10px;
  -moz-border-radius-bottomleft: 10px;
  -moz-border-radius-bottomright: 10px;
  border-radius-topright: 10px;
  border-radius-bottomleft: 10px;
  border-radius-bottomright: 10px;
}

ul.controlList {
  list-style-type: none;
}

li[aria-selected='true'] {
  color: black;
  background-color: #fff;
  border-bottom: 1px solid white;
}

div[aria-hidden='true'] {
   display: none;
}

.focus {
  margin-top: 0;
  height: 1.2em;
}

.accordian {
  margin: 0;
  float: none;
  -webkit-border-radius: 0;
  -moz-border-radius: 0;
  border-radius: 0;
  width: 600px;
}

.hidden {
  position: absolute;
  left: -300em;
  top: -30em;
}

Javascript Source Code


$(document).ready(function() {

  var panel1 = new tabpanel("tabpanel1", false);
});

//
// keyCodes() is an object to contain keycodes needed for the application
//
function keyCodes() {
  // Define values for keycodes
  this.tab        = 9;
  this.enter      = 13;
  this.esc        = 27;

  this.space      = 32;
  this.pageup     = 33;
  this.pagedown   = 34;
  this.end        = 35;
  this.home       = 36;

  this.left       = 37;
  this.up         = 38;
  this.right      = 39;
  this.down       = 40;

} // end keyCodes

//
// tabpanel() is a class constructor to create a ARIA-enabled tab panel widget.
//
// @param (id string) id is the id of the div containing the tab panel.
//
// @param (accordian boolean) accordian is true if the tab panel should operate
//         as an accordian; false if a tab panel
//
// @return N/A
//
// Usage: Requires a div container and children as follows:
//
//         1. tabs/accordian headers have class 'tab'
//
//         2. panels are divs with class 'panel'
//
function tabpanel(id, accordian) {

  // define the class properties
  
  this.panel_id = id; // store the id of the containing div
  this.accordian = accordian; // true if this is an accordian control
  this.$panel = $('#' + id);  // store the jQuery object for the panel
  this.keys = new keyCodes(); // keycodes needed for event handlers
  this.$tabs = this.$panel.find('.tab'); // Array of panel tabs.
  this.$panels = this.$panel.children('.panel'); // Array of panels.

  // Bind event handlers
  this.bindHandlers();

  // Initialize the tab panel
  this.init();

} // end tabpanel() constructor

//
// Function init() is a member function to initialize the tab/accordian panel. Hides all panels. If a tab
// has the class 'selected', makes that panel visible; otherwise, makes first panel visible.
//
// @return N/A
//
tabpanel.prototype.init = function() {
  var $tab; // the selected tab - if one is selected

  // add aria attributes to the panels
  this.$panels.attr('aria-hidden', 'true');

  // get the selected tab
  $tab = this.$tabs.filter('[aria-selected="true"]');

  if ($tab == undefined) {
    $tab = this.$tabs.first();
  }

  // show the panel that the selected tab controls and set aria-hidden to false
  this.$panel.find('#' + $tab.attr('aria-controls')).attr('aria-hidden', 'false');

} // end init()

//
// Function switchTabs() is a member function to give focus to a new tab or accordian header.
// If it's a tab panel, the currently displayed panel is hidden and the panel associated with the new tab
// is displayed.
//
// @param ($curTab obj) $curTab is the jQuery object of the currently selected tab
//
// @param ($newTab obj) $newTab is the jQuery object of new tab to switch to
//
// @return N/A
//
tabpanel.prototype.switchTabs = function($curTab, $newTab) {

  // Remove the highlighting from the current tab
  $curTab.removeClass('focus');

  // remove tab from the tab order and update its aria-selected attribute
  $curTab.attr('tabindex', '-1').attr('aria-selected', 'false');

  // update the aria attributes
  
  // Highlight the new tab and update its aria-selected attribute
  $newTab.attr('aria-selected', 'true');

  // If this is a tab panel, swap displayed tabs
  if (this.accordian == false) {
    // hide the current tab panel and set aria-hidden to true
    this.$panel.find('#' + $curTab.attr('aria-controls')).attr('aria-hidden', 'true');

    // show the new tab panel and set aria-hidden to false
    this.$panel.find('#' + $newTab.attr('aria-controls')).attr('aria-hidden', 'false');
  }

  // Make new tab navigable
  $newTab.attr('tabindex', '0');

  // give the new tab focus
  $newTab.focus();

} // end switchTabs()

//
// Function togglePanel() is a member function to display or hide the panel associated with an accordian header
//
// @param ($tab obj) $tab is the jQuery object of the currently selected tab
//
// @return N/A
//
tabpanel.prototype.togglePanel = function($tab) {

  $panel = this.$panel.find('#' + $tab.attr('aria-controls'));

  if ($panel.attr('aria-hidden') == 'true') {
    $panel.slideDown(100);
    $panel.attr('aria-hidden', 'false');
  }
  else {
    $panel.slideUp(100);
    $panel.attr('aria-hidden', 'true');
  }
} // end togglePanel()

//
// Function bindHandlers() is a member function to bind event handlers for the tabs
//
// @return N/A
//
tabpanel.prototype.bindHandlers = function() {

  var thisObj = this; // Store the this pointer for reference

  //////////////////////////////
  // Bind handlers for the tabs / accordian headers

  // bind a tab keydown handler
  this.$tabs.keydown(function(e) {
    return thisObj.handleTabKeyDown($(this), e);
  });

  // bind a tab keypress handler
  this.$tabs.keypress(function(e) {
    return thisObj.handleTabKeyPress($(this), e);
  });

  // bind a tab click handler
  this.$tabs.click(function(e) {
    return thisObj.handleTabClick($(this), e);
  });

  // bind a tab focus handler
  this.$tabs.focus(function(e) {
    return thisObj.handleTabFocus($(this), e);
  });

  // bind a tab blur handler
  this.$tabs.blur(function(e) {
    return thisObj.handleTabBlur($(this), e);
  });

  /////////////////////////////
  // Bind handlers for the panels
  
  // bind a keydown handlers for the panel focusable elements
  this.$panels.keydown(function(e) {
    return thisObj.handlePanelKeyDown($(this), e);
  });

  // bind a keypress handler for the panel
  this.$panels.keypress(function(e) {
    return thisObj.handlePanelKeyPress($(this), e);
  });

} // end bindHandlers()

//
// Function handleTabKeyDown() is a member function to process keydown events for a tab
//
// @param ($tab obj) $tab is the jquery object of the tab being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handleTabKeyDown = function($tab, e) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.space: {

      // Only process if this is an accordian widget
      if (this.accordian == true) {
        // display or collapse the panel
        this.togglePanel($tab);

        e.stopPropagation();
        return false;
      }

      return true;
    }
    case this.keys.left:
    case this.keys.up: {

      var thisObj = this;
      var $prevTab; // holds jQuery object of tab from previous pass
      var $newTab; // the new tab to switch to

      if (e.ctrlKey) {
        // Ctrl+arrow moves focus from panel content to the open
        // tab/accordian header.
      }
      else {
        var curNdx = this.$tabs.index($tab);

        if (curNdx == 0) {
          // tab is the first one:
          // set newTab to last tab
          $newTab = this.$tabs.last();
        }
        else {
          // set newTab to previous
          $newTab = this.$tabs.eq(curNdx - 1);
        }

        // switch to the new tab
        this.switchTabs($tab, $newTab);
      }

      e.stopPropagation();
      return false;
    }
    case this.keys.right:
    case this.keys.down: {

      var thisObj = this;
      var foundTab = false; // set to true when current tab found in array
      var $newTab; // the new tab to switch to

      var curNdx = this.$tabs.index($tab);

      if (curNdx == this.$tabs.length-1) {
        // tab is the last one:
        // set newTab to first tab
        $newTab = this.$tabs.first();
      }
      else {
        // set newTab to next tab
        $newTab = this.$tabs.eq(curNdx + 1);
      }

      // switch to the new tab
      this.switchTabs($tab, $newTab);

      e.stopPropagation();
      return false;
    }
    case this.keys.home: {

      // switch to the first tab
      this.switchTabs($tab, this.$tabs.first());

      e.stopPropagation();
      return false;
    }
    case this.keys.end: {

      // switch to the last tab
      this.switchTabs($tab, this.$tabs.last());

      e.stopPropagation();
      return false;
    }
  }
} // end handleTabKeyDown()

//
// Function handleTabKeyPress() is a member function to process keypress events for a tab.
//
//
// @param ($tab obj) $tab is the jquery object of the tab being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handleTabKeyPress = function($tab, e) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
    case this.keys.enter:
    case this.keys.space:
    case this.keys.left:
    case this.keys.up:
    case this.keys.right:
    case this.keys.down:
    case this.keys.home:
    case this.keys.end: {
      e.stopPropagation();
      return false;
    }
    case this.keys.pageup:
    case this.keys.pagedown: {

      // The tab keypress handler must consume pageup and pagedown
      // keypresses to prevent Firefox from switching tabs
      // on ctrl+pageup and ctrl+pagedown

      if (!e.ctrlKey) {
        return true;
      }

      e.stopPropagation();
      return false;
    }
  }

  return true;

} // end handleTabKeyPress()

//
// Function handleTabClick() is a member function to process click events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabClick = function($tab, e) {

   // hide the panels
   this.$panels.attr('aria-hidden', 'true');

  // remove all tabs from the tab order and reset their aria-selected attribute
  this.$tabs.attr('tabindex', '-1').attr('aria-selected', 'false');

  // Update the selected tab's aria-selected attribute
  $tab.attr('aria-selected', 'true');

  // show the clicked tab panel
  this.$panel.find('#' + $tab.attr('aria-controls')).attr('aria-hidden', 'false');

  // make clicked tab navigable
  $tab.attr('tabindex', '0');

  // give the tab focus
  $tab.focus();

  return true;

} // end handleTabClick()

//
// Function handleTabFocus() is a member function to process focus events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabFocus = function($tab, e) {

  // Add the focus class to the tab
  $tab.addClass('focus');

  return true;

} // end handleTabFocus()

//
// Function handleTabBlur() is a member function to process blur events for tabs
//
// @param ($tab object) $tab is the jQuery object of the tab being processed
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
tabpanel.prototype.handleTabBlur = function($tab, e) {

  // Remove the focus class to the tab
  $tab.removeClass('focus');

  return true;

} // end handleTabBlur()


/////////////////////////////////////////////////////////
// Panel Event handlers
//

//
// Function handlePanelKeyDown() is a member function to process keydown events for a panel
//
// @param ($elem obj) $elem is the jquery object of the element being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyDown = function($elem, e) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
    case this.keys.esc: {
      e.stopPropagation();
      return false;
    }
    case this.keys.left:
    case this.keys.up: {

      if (!e.ctrlKey) {
        // do not process
        return true;
      }
  
      // get the jQuery object of the tab
      var $tab = $('#' + $elem.attr('aria-labelledby'));

      // Move focus to the tab
      $tab.focus();

      e.stopPropagation();
      return false;
    }
    case this.keys.pageup: {

      var $newTab;

      if (!e.ctrlKey) {
        // do not process
        return true;
      }

      // get the jQuery object of the tab
      var $tab = this.$tabs.filter('[aria-selected="true"]');

      // get the index of the tab in the tab list
      var curNdx = this.$tabs.index($tab);

      if (curNdx == 0) {
        // this is the first tab, set focus on the last one
        $newTab = this.$tabs.last();
      }
      else {
        // set focus on the previous tab
        $newTab = this.$tabs.eq(curNdx - 1);
      }

      // switch to the new tab
      this.switchTabs($tab, $newTab);

      e.stopPropagation();
      e.preventDefault();
      return false;
    }
    case this.keys.pagedown: {

      var $newTab;

      if (!e.ctrlKey) {
        // do not process
        return true;
      }

      // get the jQuery object of the tab
      var $tab = $('#' + $elem.attr('aria-labelledby'));

      // get the index of the tab in the tab list
      var curNdx = this.$tabs.index($tab);

      if (curNdx == this.$tabs.length-1) {
        // this is the last tab, set focus on the first one
        $newTab = this.$tabs.first();
      }
      else {
        // set focus on the next tab
        $newTab = this.$tabs.eq(curNdx + 1);
      }

      // switch to the new tab
      this.switchTabs($tab, $newTab);

      e.stopPropagation();
      e.preventDefault();
      return false;
    }
  }

  return true;

} // end handlePanelKeyDown()

//
// Function handlePanelKeyPress() is a member function to process keypress events for a panel
//
// @param ($elem obj) $elem is the jquery object of the element being processed
//
// @param (e obj) e is the associated event object
//
// @return (boolean) Returns true if propagating; false if consuming event
//
tabpanel.prototype.handlePanelKeyPress = function($elem, e) {

  if (e.altKey) {
    // do nothing
    return true;
  }

  if (e.ctrlKey && (e.keyCode == this.keys.pageup || e.keyCode == this.keys.pagedown)) {
      e.stopPropagation();
      e.preventDefault();
      return false;
  }

  switch (e.keyCode) {
    case this.keys.esc: {
      e.stopPropagation();
      e.preventDefault();
      return false;
    }
  }

  return true;

} // end handlePanelKeyPress()

// focusable is a small jQuery extension to add a :focusable selector. It is used to
// get a list of all focusable elements in a panel. Credit to ajpiano on the jQuery forums.
//
$.extend($.expr[':'], {
  focusable: function(element) {
    var nodeName = element.nodeName.toLowerCase();
    var tabIndex = $(element).attr('tabindex');

    // the element and all of its ancestors must be visible
    if (($(element)[nodeName == 'area' ? 'parents' : 'closest'](':hidden').length) == true) {
      return false;
    }

    // If tabindex is defined, its value must be greater than 0
    if (!isNaN(tabIndex) && tabIndex < 0) {
      return false;
    }

    // if the element is a standard form control, it must not be disabled
    if (/input|select|textarea|button|object/.test(nodeName) == true) {

             return !element.disabled;
    }

    // if the element is a link, href must be defined
    if ((nodeName == 'a' ||  nodeName == 'area') == true) {

      return (element.href.length > 0);
    }
            
    // this is some other page element that is not normally focusable.
    return false;
  }
});