Summary

There are several techniques for creating tables with static header rows, but very few of the examples I have come across are accessible or do not work cross-browser. This post looks at some of the current solutions, and outlines a method using WAI-ARIA to fix the accessibility issues of the more popular techniques.

Author: Gez Lemon

Current techniques

Ideally, this problem should be solved using just CSS by specifying the height, overflow, and display properties on the body of the table (tbody). This works to an extent, but is difficult to get working cross-browser. Pure CSS Scrollable Table with Fixed Header is an old but good example of a CSS version that works in most browsers and screen readers, but doesn't render properly in IE9.

Most solutions use JavaScript, and typically duplicate the header of the table into a separate table. jQuery Plugin for Fixed Header Tables is a good example of this technique, but there are plenty of other examples that essentially do the same. This technique is popular, because it's easier to get to work cross-browser. The accessibility of this solution is not so good, as it results in a separate table just for the headers. Most of the solutions that attempt to remain accessible duplicate the headers in the scrolling table and hide them visually so they are still available to screen readers; but there is still a redundant table in the content containing just the headers.

WAI-ARIA as a repair technique

As having two tables seems to be the most reliable in terms of portability, one solution would be to repair the technique so that it appears as a single table to assistive technologies. Consider a data table that determines calories burned depending on the weight and pace of a runner with the headers in a completely different table so that the main body can be set to scroll, as most approaches appear to use this or a variation on this technique.

Table with static headers
Table scrolled slightly
Typical markup with two separate data tables
<table>
<tr>
  <th>Pace</th>
  <th>59KG</th>
  <th>74KG</th>
  <th>86KG</th>
</tr>
</table>

<table>
⋯
<tr>
  <th>12 min/mile</th>
  <td>472 cal/hour</td>
  <td>582 cal/hour</td>
  <td>691 cal/hour</td>
</tr>
<tr>
  <th>11 min/mile</th>
  <td>532 cal/hour</td>
  <td>655 cal/hour</td>
  <td>734 cal/hour</td>
</tr>
<tr>
  <th>10 min/mile</th>
  <td>591 cal/hour</td>
  <td>727 cal/hour</td>
  <td>864 cal/hour</td>
</tr>
⋯
</table>

The process to repair and merge the table with WAI-ARIA is outlined below:

The following is the resulting structure:

Using WAI-ARIA to merge two tables to create a single table
<h2 id="calburn">Calorie burn by pace and weight</h2>
<div role="grid"
     aria-labelledby="calburn"
     aria-readonly="true">
  <table role="presentation">
  <tr role="row">
    <th role="columnheader">Pace</th>
    <th role="columnheader">59KG</th>
    <th role="columnheader">74KG</th>
    <th role="columnheader">86KG</th>
  </tr>
  </table>

  <table role="presentation">
⋯
  <tr role="row">
    <th role="rowheader">12 min/mile</th>
    <td role="gridcell">472 cal/hour</td>
    <td role="gridcell">582 cal/hour</td>
    <td role="gridcell">691 cal/hour</td>
  </tr>
  <tr role="row">
    <th role="rowheader">11 min/mile</th>
    <td role="gridcell">532 cal/hour</td>
    <td role="gridcell">655 cal/hour</td>
    <td role="gridcell">734 cal/hour</td>
  </tr>
  <tr role="row">
    <th role="rowheader">10 min/mile</th>
    <td role="gridcell">591 cal/hour</td>
    <td role="gridcell">727 cal/hour</td>
    <td role="gridcell">864 cal/hour</td>
  </tr>
⋯
  </table>
</div>

Alternatively, the structure could be defined with WAI-ARIA removing the HTML tables completely, as it's far easier to control the div elements with CSS than the tables.

Using WAI-ARIA alone to define a data table
<h2 id="calburn">Calorie burn by pace and weight</h2>
<div role="grid"
     aria-labelledby="calburn"
     aria-readonly="true">
  <div role="row">
    <span role="columnheader">Pace</span>
    <span role="columnheader">59KG</span>
    <span role="columnheader">74KG</span>
    <span role="columnheader">86KG</span>
  </div>
  <div id="scrolling">
⋯
    <div role="row">
      <span role="rowheader">12 min/mile</span>
      <span role="gridcell">472 cal/hour</span>
      <span role="gridcell">582 cal/hour</span>
      <span role="gridcell">691 cal/hour</span>
    </div>
    <div role="row">
      <span role="rowheader">11 min/mile</span>
      <span role="gridcell">532 cal/hour</span>
      <span role="gridcell">655 cal/hour</span>
      <span role="gridcell">734 cal/hour</span>
    </div>
    <div role="row">
      <span role="rowheader">10 min/mile</span>
      <span role="gridcell">591 cal/hour</span>
      <span role="gridcell">727 cal/hour</span>
      <span role="gridcell">864 cal/hour</span>
    </div>
⋯
  </div>
</div>

Accessibility of this solution

This solution works well using JAWS. Unfortunately, it does not work well with VoiceOver on an iPad iOS6 or on a Mac. VoiceOver recognises the table as a single table, but does not map the columnheader or rowheader roles as column and row headers properly. In iOS6 they're ignored, and on a Mac they're reported incorrectly (thanks to Steve Faulkner for testing these roles on a Mac with VoiceOver).

Further, the W3C validator currently reports an error for columnheader and rowheader roles contained in a row, but a bug has been filed and it is expected that this issue will be resolved shortly.

Conclusion

The best solution would be to use CSS to scroll the main body while leaving the headers static, but there doesn't appear to be an accessible solution that works cross-browser. If anyone knows of a solution, please post in the comments. As the most popular technique to have static headers in a data table is to put the headers and the table content in two different tables, then this repair technique seems reasonable (or defining the structure with WAI-ARIA alone). It's a shame the WAI-ARIA columnheader and rowheader roles are not better supported in VoiceOver, but support is likely to improve.

Category: Accessibility.

Comments

  1. [accessible_data_tables_static_headers.php#comment1]

    The problem with CSS-only solutions is that they're not accessible to voice dictation users. The scrollbar that is created does not respond to voice commands.

    We played with these a bit and came up with this idea: http://examples.simplyaccessible.com/scrollable-table/slider.html

    The challenge remains that voice dictation users wouldn't know what voice commands to use to call it, which is still something we're looking into. At this point, however, at least once the item has focus a voice dictation user can manipulate the slider.

    Posted by Karl Groves on

  2. [accessible_data_tables_static_headers.php#comment2]

    Hi Karl,

    Thank you for your insightful comments and great point about voice commands with pure CSS solutions. The example doesn't work properly for me using the keyboard in IE8 (the thumb moves up and down, but the table remains static), and it doesn't render properly in IE9, but I understand it's a work in progress. It works well with JAWS, Firefox and Chrome.

    Posted by Gez on

  3. [accessible_data_tables_static_headers.php#comment4]

    I wonder if there was any CSS associated with the example tables when testing with Safari. Table recognition is problematic if there is no border. Voiceover requires an explicit border.

    @Karl We're working with Nuance to get better support for accessibility APIs in browsers. That should make voice recognition interaction easier.

    Posted by Pratik Patel on

  4. [accessible_data_tables_static_headers.php#comment7]

    @Pratik

    "We're working with Nuance to get better support for accessibility APIs in browsers. That should make voice recognition interaction easier."

    That's great to know, as the lack of support for standards in such products causes much grief.

    Posted by steve faulkner on

  5. [accessible_data_tables_static_headers.php#comment8]

    I've used the following before to solve this problem without using role=grid.

    The "Visible" table header which is intended to remain static is hidden from AT using aria-hidden="true" and role="presentation" (really just a fallback in case hidden isn't supported)

    The Screen reader table header row is hidden off-screen

    The Links (intended to enable sorting - not functional in this example) are made non-focusable in the "Visible" header using tabindex=-1

    The focusable links are off-screen so we use JavaScript to simulate a focus on the "visible" header

    onclick events to do the sorting are needed on both headers (not supplied in the linked example)

    http://jnurthen.users.sonic.net/hiddenTableHeader.html

    Some imagination is required visually as I have made no attempt to size the rows in the Static table with the cells in the actual data table.

    Posted by James Nurthen on

  6. [accessible_data_tables_static_headers.php#comment9]

    Thanks, James. This information is very useful. I've tested it and it works well.

    If aria-hidden="true" fails, role="presentation" wouldn't be useful, as there would still be a redundant information in the content (the table semantics would be removed, but the content would still be available).

    I appreciate you sharing this, as this is a very good solution. Thanks.

    Posted by Gez on

  7. [accessible_data_tables_static_headers.php#comment10]

    @Gez I put the role=presentation there thinking that some erroneous text is better than an erroneous table. Thinking about it more a 1 by X table gets treated as presentational by most AT anyway so role=presentation is completely unnecessary.

    Posted by James Nurthen on

  8. [accessible_data_tables_static_headers.php#comment13]

    The table can be transformed to 2 columns only, repeating headers, using only CSS, custom attributes and before/content pointing to said attribute. Still quite annoying HTML, not friendly to WYSIWYG, and not 100% accurate with widths. I'd prefer to analyse each individual project individually because the data displayed on the desktop might not be interesting to show on a mobile, ie the mobile to use a cut down version only.

    Posted by Steve on

Comments are closed for this entry.