O'Reilly logo

JavaScript Cookbook by Shelley Powers

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

14.7. Adding ARIA Attributes to a Tabbed Page Application

Problem

You want to split the page contents into separate panels, and only display one at a time. You also want the application to be accessible.

Solution

Use a tabbed page application and include the ARIA roles tablist, tabpanel, and tab, as well as the aria-hidden state.

The tabbed page is a div element, as is the container, with the tabs as list items (li) within a single unordered list (ul) at the top. The container div is given a role of tablist, and each of the li elements given a role of tab. The tabbed panels are div elements containing whatever type of contents, and each is given a role of tabpanel. The relationship between panel and tab is made with the aria-labeledby attribute:

<div class="tabcontainer" role="tablist">
   <div class="tabnavigation" role="tab">
      <ul>
         <li id="tabnav_1" role="tab"><a href="">Page One</a></li>
         <li id="tabnav_2" role="tab"><a href="">Page Two</a></li>
         <li id="tabnav_3" role="tab"><a href="">Page Three</a></li>
      </ul>
   </div>

   <div class="tabpages">
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_1"
aria-hidden="false" id="tabpage_1">
         <p>page 1</p>
      </div>
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_2"
aria-hidden="true" id="tabpage_2">
         <p>page 2</p>
      </div>
      <div class="tabpage" role="tabpanel" aria-labeledby="tabnav_3"
aria-hidden="true" id="tabpage_3">
         <p>page 3</p>
      </div>
   </div>
</div>

When the page loads, the tabs are displayed and all but the first of the panels are hidden. The hidden panels are assigned an aria-hidden attribute value of true. The click event handler for all of the tab elements is assigned a function, displayPage, to control the tab page display. A custom data attribute, data-current, on the tabbed page container is used to store which tab is currently selected:

// set up display
// for each container display navigation
// hide all but first page, highlight first tab
window.onload=function() {

  // for each container
  var containers = document.querySelectorAll(".tabcontainer");
  for (var j = 0; j < containers.length; j++) {

    // display and hide elements
    var nav = containers[j].querySelector(".tabnavigation ul");
    nav.style.display="block";

    // set current tab
   var navitem = containers[j].querySelector(".tabnavigation ul li");
    var ident = navitem.id.split("_")[1];
    navitem.parentNode.setAttribute("data-current",ident);
    navitem.setAttribute("style","background-color: #ccf");

    // set displayed tab panel
    var pages = containers[j].querySelectorAll(".tabpage");
    for (var i = 1; i < pages.length; i++) {
      pages[i].style.display="none";
      pages[i].setAttribute("aria-hidden","true");
    }

    // for each tab, attach event handler function
   var tabs = containers[j].querySelectorAll(".tabnavigation ul li");
    for (var i = 0; i < tabs.length; i++) {
      tabs[i].onclick=displayPage;
    }
  }
}

When a tab is clicked, the old tabbed entry is cleared by setting the background of the tab to white and hiding the panel. The new entry’s tab is highlighted (background color is changed), and its associated panel is displayed. The hidden panel’s aria-hidden attribute is set to true, while the displayed panel’s aria-hidden attribute is set to false. The custom data attribute data-current is set to the new tab selection, and the clicked tab’s id is used to derive the related panel’s ID:

// click on tab
function displayPage() {

  // hide old selection
  var current = this.parentNode.getAttribute("data-current");
  var oldpanel = document.getElementById("tabpage_" + current);

  document.getElementById("tabnav_" + current).setAttribute("style",
  "background-color: #fff");
  oldpanel.style.display="none";
  oldpanel.setAttribute("aria-hidden","true");

  // display new selection
  var ident = this.id.split("_")[1];
  this.setAttribute("style","background-color: #ccf");
  var newpanel = document.getElementById("tabpage_" + ident);

  newpanel.style.display="block";
  newpanel.setAttribute("aria-hidden","false");
  this.parentNode.setAttribute("data-current",ident);

  return false;
}

Discussion

The code in the solution is very similar to that in Recipe 13.7; the only difference is the addition of the ARIA roles and attributes. The changes are minor—I highlighted the lines in the JavaScript that were added to enable ARIA support. As you can see, adding accessibility with ARIA is not an onerous task.

Another excellent demonstration of an ARIA-enabled tabbed page can be found at the Illinois Center for Information Technology and Web Accessibility. Though the code to manage the tabbed page behavior differs significantly from mine, the relative structure and use of ARIA roles and attributes is identical. The main difference between the two implementations is that my example uses a link to make my tabs clickable, while the external example uses tabindex. The external application also makes more extensive use of the keyboard.

As mentioned in Recipe 14.5, working with the ARIA attributes provides a new way of looking at widget-like applications like tabbed pages and accordions. The Illinois Center I just mentioned actually lists both the accordion and tabbed page example in one section specific to tabbed pages, because they have very similar behavior. The only difference is one is multiselectable, the other not; one requires modification to the label to signify which label is associated with which panel, while the other does not need this information. By looking at both types of widgets with a fresh viewpoint, I’ve learned new ways to use both: instead of creating vertical accordion panels, I’ve also started using horizontal panels; rather than tabs being located at the top of a tabbed application, I’ve started placing them on the side. It taught me to appreciate how they are both semantically linked, and how to ensure this semantic similarity is preserved in both structure and code.

See Also

Recipe 14.4 covers some of the implementation details with using tabindex.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required