Example Start

Animal, Mineral, or Vegetable

Group expanded Animals
Birds
Siamese
Tabby
Group expanded Dogs
Group expanded Small Breeds
Chihuahua
Italian Greyhound
Japanese Chin
Beagle
Cocker Spaniel
Pit Bull
Afghan
Great Dane
Mastiff
Group expanded Minerals
Zinc
Yellow Gold
White Gold
Silver
Group expanded Vegetables
Carrot
Tomato
Lettuce

Example End

Example Description

Type: Best Practice

Simple example of a treeview widget using aria-owns to define markup relationships.

Keyboard Support

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


  • Up: Select the previous visible tree item.

  • Down: Select next visible tree item.

  • Left: Collapse the currently selected parent node if it is expanded. Move to the previous parent node (if possible) when the current parent node is collapsed.

  • Right: Expand the currently selected parent node and move to the first child list item.

  • Enter: Toggle the expanded or collapsed state of the selected parent node.

  • Home: Select the root parent node of the tree.

  • End: Select the last visible node of the tree.

  • Tab: Navigate away from tree.

  • * (asterisk on the numpad): Expand all group nodes.

  • Double-clicking on a parent node will toggle its expanded or collapsed state.

Example Markup

Browser Compatibility

HTML Source Code


<div id="application" role="application">

<h2 id="label_1">Animal, Mineral, or Vegetable</h2>
<div id="tree1" class="tree root-level" role="tree" aria-labelledby="label_1" tabindex="-1">
   <div id="animals" class="tree-parent" role="treeitem" aria-owns="animalGroup" aria-expanded="true" tabindex="0">
      <img class="parentImg" role="treeitem" src="http://www.oaa-accessibility.org/media/examples/images/expanded.gif" alt="Group expanded"/>
      <span>Animals</span>
   </div>
   <div id="animalGroup" class="group" role="group">
      <div id="birds" class="tree-item" role="treeitem" tabindex="-1">Birds</div>

      <div id="cats" class="tree-parent" role="treeitem" aria-owns="catGroup" aria-expanded="false" tabindex="-1">
         <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/contracted.gif" alt="Group collapsed"/>
         <span>Cats</span>
      </div>
      <div id="catGroup" class="group" role="group">
         <div id="siamese" class="tree-item" role="treeitem" tabindex="-1">Siamese</div>
         <div id="tabby" class="tree-item" role="treeitem" tabindex="-1">Tabby</div>
      </div>
      <div id="dogs" class="tree-parent" role="treeitem" aria-owns="dogGroup" aria-expanded="true" tabindex="-1">
         <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/expanded.gif" alt="Group expanded"/>
         <span>Dogs</span>
      </div>
      <div id="dogGroup" class="group" role="group">
         <div id="smallBreeds" class="tree-parent" role="treeitem" aria-owns="smallBreedGroup" aria-expanded="true" tabindex="-1">
            <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/expanded.gif" alt="Group expanded"/>
            <span>Small Breeds</span>
         </div>
         <div id="smallBreedGroup" class="group" role="group">
            <div id="chihuahua" class="tree-item" role="treeitem" tabindex="-1">Chihuahua</div>
            <div id="italian_greyhound" class="tree-item" role="treeitem" tabindex="-1">Italian Greyhound</div>
            <div id="Japanese_chin" class="tree-item" role="treeitem" tabindex="-1">Japanese Chin</div>
         </div>
         <div id="mediumBreeds" class="tree-parent" role="treeitem" aria-owns="mediumBreedGroup" aria-expanded="false" tabindex="-1">
            <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/contracted.gif" alt="Group collapsed"/>
            <span>Medium Breeds</span>
         </div>
         <div id="mediumBreedGroup" class="group" role="group">
            <div id="beagle" class="tree-item" role="treeitem" tabindex="-1">Beagle</div>
            <div id="cocker_spaniel" class="tree-item" role="treeitem" tabindex="-1">Cocker Spaniel</div>
            <div id="pit_bull" class="tree-item" role="treeitem" tabindex="-1">Pit Bull</div>
         </div>
         <div id="largeBreeds" class="tree-parent" role="treeitem" aria-owns="largeBreedGroup" aria-expanded="false" tabindex="-1">
            <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/contracted.gif" alt="Group collapsed"/>
            <span>Large Breeds</span>
         </div>
         <div id="largeBreedGroup" class="group" role="group">
            <div id="afghan" class="tree-item" role="treeitem", tabindex="-1">Afghan</div>
            <div id="great_dane" class="tree-item" role="treeitem" tabindex="-1">Great Dane</div>
            <div id="mastiff" class="tree-item" role="treeitem" tabindex="-1">Mastiff</div>
         </div>
      </div>
   </div>
   <div id="minerals" class="tree-parent" role="treeitem" aria-owns="mineralGroup" aria-expanded="true" tabindex="-1">
      <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/expanded.gif" alt="Group expanded"/>
      <span>Minerals</span>
   </div>
   <div id="mineralGroup" class="group" role="group">
      <div id="zinc" class="tree-item" role="treeitem" tabindex="-1">Zinc</div>
      <div id="gold" class="tree-parent" role="treeitem" aria-owns="goldGroup" aria-expanded="false" tabindex="-1">
         <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/contracted.gif" alt="Group collapsed"/>
         <span>Gold</span>
      </div>
      <div id="goldGroup" class="group" role="group">
         <div id="yellow_gold" class="tree-item" role="treeitem" tabindex="-1">Yellow Gold</div>
         <div id="white_gold" class="tree-item" role="treeitem" tabindex="-1">White Gold</div>
      </div>
      <div id="silver" class="tree-item" role="treeitem" tabindex="-1">Silver</div>
   </div>
   <div id="vegetables" class="tree-parent" role="treeitem" aria-owns="vegetableGroup" aria-expanded="true" tabindex="-1">
      <img class="parentImg" role="treeitem" tabindex="-1" src="http://www.oaa-accessibility.org/media/examples/images/expanded.gif" alt="Group expanded"/>
      <span>Vegetables</span>
   </div>
   <div id="vegetableGroup" class="group" role="group">
      <div id="carrot" class="tree-item" role="treeitem" tabindex="-1">Carrot</div>
      <div id="tomato" class="tree-item" role="treeitem" tabindex="-1">Tomato</div>
      <div id="lettuce" class="tree-item" role="treeitem" tabindex="-1">Lettuce</div>
   </div>
</div>

</div>

CSS Code


h2#label_1 {
margin: .5em 0 !important;
padding: 0 !important;
font-size: 1.6em !important;
}
div.tree {
  margin-left: 20px;
  padding: 0;
  width: 15em;
}
div.group {
  padding-left: 22px;
}
div.tree-item {
  padding-left: 22px;
}
div.tree-parent {
  font-weight: bold;
}

img.parentImg {
  margin-right: 5px;
}

div.tree-focus {
  color: white;
  background: black;
}

Javascript Source Code


$(document).ready(function() {

  var treeviewApp = new treeview('tree1');

}); // end ready

//
// Function treeview() is a class constructor for a treeview widget. The widget binds to an
// unordered list. The top-level <ul> must have role='tree'. All tree items must have role='treeitem'.
//
// Tree groups must be embedded lists within the listitem that heads the group. the top <ul> of a group
// must have role='group'. aria-expanded is used to indicate whether a group is expanded or collapsed. This
// property must be set on the listitem the encapsulates the group.
//
// @param (treeID string) treeID is the html id of the top-level <ul> of the list to bind the widget to
//
// @return N/A
//
function treeview(treeID) {

  // define the object properties
  this.$id = $('#' + treeID);
  this.$listItems = this.$id.find('div').not('.group'); // an array of tree items
  this.$parents = this.$id.find('.tree-parent'); // an array of the parent items
  this.$visibleItems = undefined; // an array of currently visible tree Items (including parents)
   this.$activeItem = null; // holds the jQuery object of the active tree item

   this.keys = {
            tab:      9,
            enter:    13,
            space:    32,
            pageup:   33,
            pagedown: 34,
            end:      35,
            home:     36,
            left:     37,
            up:       38,
            right:    39,
            down:     40,
            asterisk: 106
   };

  // initialize the treeview
  this.init();

  // bind event handlers
  this.bindHandlers();

} // end treeview() constructor

//
// Function init() is a member function to initialize the treeview widget. It traverses the tree, identifying
// which listItems are headers for groups and applying initial collapsed are expanded styling
//
// @return N/A
//
treeview.prototype.init = function() {

  var thisObj = this;

  // iterate through the tree and apply the styling to the tree parents
  this.$parents.each (function(index) {

    var $group = $('#' + $(this).attr('aria-owns'));

    // If the aria-expanded is false, hide the group and display the collapsed state image
    if ($(this).attr('aria-expanded') == 'false') {
      $group.hide().attr('aria-hidden', 'true');
      $(this).find('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted.gif').attr('alt', 'Group collapsed');
    }
  });

  // create the initial visible item array
  this.$visibleItems = this.$listItems.filter(':visible');

} // end init()

//
// Function expandGroup() is a member function to expand a collapsed group
//
// @param($item object) $item is the jquery id of the parent item of the group to expand
//
// @param(hasFocus boolean) focus is true if the parent item has focus, false otherwise
//
// @return N/A
//
treeview.prototype.expandGroup = function($item, hasFocus) {

  var $group = $('#' + $item.attr('aria-owns'));

  // expand the group
  $group.show().attr('aria-hidden', 'false');

  $item.attr('aria-expanded', 'true');

  if (hasFocus == true) {
    $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded-focus.gif').attr('alt', 'Group expanded');
  }
  else {
    $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded.gif').attr('alt', 'Group expanded');
  }

  // refresh the list of visible items
  this.$visibleItems = this.$listItems.filter(':visible');

} // end expandGroup()

//
// Function collapseGroup() is a member function to collapse an expanded group
//
// @param($item object) $item is the jquery id of the parent item of the group to collapse
//
// @param(hasFocus boolean) focus is true if the parent item has focus, false otherwise
//
// @return N/A
//
treeview.prototype.collapseGroup = function($item, hasFocus) {

  var $group = $('#' + $item.attr('aria-owns'));

  // collapse the group
  $group.hide().attr('aria-hidden', 'true');

  $item.attr('aria-expanded', 'false');

  if (hasFocus == true) {
    $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted-focus.gif').attr('alt', 'Group collapsed');
  }
  else {
    $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted.gif').attr('alt', 'Group collapsed');
  }

  // refresh the list of visible items
  this.$visibleItems = this.$listItems.filter(':visible');

} // end collapseGroup()

//
// Function toggleGroup() is a member function to toggle the display state of a group
//
// @param($item object) $item is the jquery id of the parent item of the group to toggle
//
// @param(hasFocus boolean) hasFocus is true if the parent item has focus, false otherwise
//
// @return N/A
//
treeview.prototype.toggleGroup = function($item, hasFocus) {

  if ($item.attr('aria-expanded') == 'true') {
    // collapse the group
    this.collapseGroup($item, hasFocus);
  }
  else {
    // expand the group
    this.expandGroup($item, hasFocus);
  }

} // end toggleGroup()

//
// Function bindHandlers() is a member function to bind event handlers to the listItems
//
// return N/A
//
treeview.prototype.bindHandlers = function() {

  var thisObj = this;

  // bind a dblclick handler to the parent items
  this.$parents.dblclick(function(e) {
    return thisObj.handleDblClick($(this), e);
  });

  // bind a click handler
  this.$listItems.click(function(e) {
    return thisObj.handleClick($(this), e);
  });

  // bind a keydown handler
  this.$listItems.keydown(function(e) {
    return thisObj.handleKeyDown($(this), e);
  });

  // bind a keypress handler
  this.$listItems.keypress(function(e) {
    return thisObj.handleKeyPress($(this), e);
  });

  // bind a focus handler
  this.$listItems.focus(function(e) {
    return thisObj.handleFocus($(this), e);
  });

  // bind a blur handler
  this.$listItems.blur(function(e) {
    return thisObj.handleBlur($(this), e);
  });

   // bind a document click handler
   $(document).click(function(e) {

         if (thisObj.$activeItem != null) {
            // remove the focus styling
            thisObj.$activeItem.removeClass('tree-focus');

            if (thisObj.$activeItem.hasClass('tree-parent') == true) {

               // this is a parent item, remove the focus image
               if (thisObj.$activeItem.attr('aria-expanded') == 'true') {
                  thisObj.$activeItem.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded.gif');
               }
               else {
                  thisObj.$activeItem.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted.gif');
               }
            }

            // set activeItem to null
            thisObj.$activeItem = null;
         }

         return true;
   });

} // end bindHandlers()

//
// Function updateStyling() is a member function to update the styling for the tree items
//
// @param ($item object) $item is the jQuery object of the item to highlight
//
// @return N/A
//
treeview.prototype.updateStyling = function($item) {

  // remove the focus highlighting from the treeview items
  // and remove them from the tab order.
  this.$listItems.removeClass('tree-focus').attr('tabindex', '-1');

  // remove the focus image from parent items
  this.$parents.each(function() {
    // add the focus image
    if ($(this).attr('aria-expanded') == 'true') {
      $(this).children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded.gif');
    }
    else {
      $(this).children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted.gif');
    }
  });

  if ($item.hasClass('tree-parent') == true) {

    // add the focus image
    if ($item.attr('aria-expanded') == 'true') {
      $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded-focus.gif');
    }
    else {
      $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted-focus.gif');
    }
  }

  // apply the focus highlighting and place the element in the tab order
  $item.addClass('tree-focus').attr('tabindex', '0');

} // end updateStyling()

//
// Function handleKeyDown() is a member function to process keydown events for the treeview items
//
// @param ($item object) $item is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleKeyDown = function($item, e) {

  var $itemGroup = $item.parent();
  var curNdx = this.$visibleItems.index($item);

  if ((e.altKey || e.ctrlKey)
       || (e.shiftKey && e.keyCode != this.keys.tab)) {
    // do nothing
    return true;
  }

  switch (e.keyCode) {
      case this.keys.tab: {
         // set activeItem to null
         this.$activeItem = null;

         // remove the focus styling
         $item.removeClass('tree-focus');

         if ($item.hasClass('tree-parent') == true) {

            // this is a parent item, remove the focus image
            if ($item.attr('aria-expanded') == 'true') {
               $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/expanded.gif');
            }
            else {
               $item.children('img').attr('src', 'http://www.oaa-accessibility.org/media/examples/images/contracted.gif');
            }
         }

         return true;
      }
    case this.keys.home: {

         // store the active item
         this.$activeItem = this.$parents.first();

         // set focus on the active item
      this.$activeItem.focus();

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

         // store the active item
         this.$activeItem = this.$visibleItems.last();

         // set focus on the active item
      this.$activeItem.focus();

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

      if ($item.hasClass('tree-parent') == false) {
        // do nothing
      }
         else {
            // toggle the display of the child group
            this.toggleGroup($item, true);
         }

      e.stopPropagation();
      return false;
    }
    case this.keys.left: {
      
      if ($item.hasClass('tree-parent') == true
            && $item.attr('aria-expanded') == 'true') {

            this.collapseGroup($item, true);
      }
      else {
        // move up to the parent

            var groupID = $itemGroup.attr('id');

            // set the parent tree item as the active item
            this.$activeItem = this.$parents.filter('[aria-owns=' + groupID + ']');

            // set focus on the active item
            this.$activeItem.focus();
      }

      e.stopPropagation();
      return false;
    }
    case this.keys.right: {
      
      if ($item.hasClass('tree-parent') == false) {
        // do nothing
      }
         else if ($item.attr('aria-expanded') == 'false') {
               this.expandGroup($item, true);
         }
         else {
            var $childGroup = $('#' + $item.attr('aria-owns'));

            // move to the first item in the child group
            this.$activeItem = $childGroup.children('div').not('group').first();

            // set focus on the active item
            this.$activeItem.focus();
         }

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

      if (curNdx > 0) {
        var $prev = this.$visibleItems.eq(curNdx - 1);

            // stroe the new active item
            this.$activeItem = $prev;

            // set focus
        $prev.focus();
      }

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

      if (curNdx < this.$visibleItems.length - 1) {
        var $next = this.$visibleItems.eq(curNdx + 1);

            // stroe the new active item
            this.$activeItem = $next;

        $next.focus();
      }
      e.stopPropagation();
      return false;
    }
    case this.keys.asterisk: {
      // expand all groups

      var thisObj = this;

      this.$parents.each(function() {
            if (thisObj.$activeItem[0] == $(this)[0]) {
               thisObj.expandGroup($(this), true);
            }
            else {
               thisObj.expandGroup($(this), false);
            }
      });

      e.stopPropagation();
      return false;
    }
  }

  return true;

} // end handleKeyDown

//
// Function handleKeyPress() is a member function to process keypress events for the treeview items
// This function is needed for browsers, such as Opera, that perform window manipulation on kepress events
// rather than keydown. The function simply consumes the event.
//
// @param ($item object) $item is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleKeyPress = function($item, e) {

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

  switch (e.keyCode) {
      case this.keys.tab: {
         return true;
      }
    case this.keys.enter:
    case this.keys.space:
    case this.keys.home:
    case this.keys.end:
    case this.keys.left:
    case this.keys.right:
    case this.keys.up:
    case this.keys.down: {
      e.stopPropagation();
      return false;
    }
      default : {
         var chr = String.fromCharCode(e.which);
         var bMatch = false;
         var itemNdx = this.$visibleItems.index($item);
         var itemCnt = this.$visibleItems.length;
         var curNdx = itemNdx + 1;

         // check if the active item was the last one on the list
         if (curNdx == itemCnt) {
            curNdx = 0;
         }

         // Iterate through the menu items (starting from the current item and wrapping) until a match is found
         // or the loop returns to the current menu item
         while (curNdx != itemNdx)  {

            var $curItem = this.$visibleItems.eq(curNdx);
            var titleChr = $curItem.text().charAt(0);
            
            if ($curItem.is('.tree-parent')) {
               titleChr = $curItem.find('span').text().charAt(0);
            }

            if (titleChr.toLowerCase() == chr) {
               bMatch = true;
               break;
            }

            curNdx = curNdx+1;

            if (curNdx == itemCnt) {
               // reached the end of the list, start again at the beginning
               curNdx = 0;
            }
         }

         if (bMatch == true) {
            this.$activeItem = this.$visibleItems.eq(curNdx);
            this.$activeItem.focus();
         }

         e.stopPropagation();
         return false;
      }
  }

  return true;

} // end handleKeyPress

//
// Function handleDblClick() is a member function to process double-click events for parent items.
// Double-click expands or collapses a group.
//
// @param ($id object) $id is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleDblClick = function($id, e) {

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

   // update the active item
   this.$activeItem = $id;

  // apply the focus highlighting
  this.updateStyling($id);

  // expand or collapse the group
  this.toggleGroup($id, true);

  e.stopPropagation();
  return false;

} // end handleDblClick

//
// Function handleClick() is a member function to process click events.
//
// @param ($item object) $item is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns false if consuming event; true if not
//
treeview.prototype.handleClick = function($item, e) {

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

   // update the active item
   this.$activeItem = $item;

  // apply the focus highlighting
  this.updateStyling($item);

  e.stopPropagation();
  return false;

} // end handleClick

//
// Function handleFocus() is a member function to process focus events.
//
// @param ($item object) $item is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleFocus = function($item, e) {


   if (this.$activeItem == null) {
      this.$activeItem = $item;
   }

   this.updateStyling(this.$activeItem);


  return true;

} // end handleFocus

//
// Function handleBlur() is a member function to process blur events.
//
// @param ($id object) $id is the jQuery id of the parent item firing event
//
// @param (e object) e is the associated event object
//
// @return (boolean) returns true
//
treeview.prototype.handleBlur = function($id, e) {

  return true;

} // end handleBlur