Every once in a while I come across a table where the header row must be in a separate table above the data table, because some particular functionality "requires it". Since many clients already have this in place, I've cooked up a little work around that seems to work fine in new screen readers. I created a ghost heading row in the data table positioning it offscreen, and placed aria-hidden="true" on the table element that contains their headings, so the screen reader ignores that one row table. Below is an example.
Ghost heading 1 | Ghost heading 2 | Ghost heading 3 | Ghost heading 4 |
---|---|---|---|
1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
99 | 9 | 0 | 11 |
<style>
.hideme {
height: 1px; border: none }
.noborder{border: none}
.offscreen {
height: 1px; width: 1px; position: absolute; overflow: hidden; top: -10px;}
.header {font-weight:bold;
text-align:center}
</style>
<table width="469" border="1" aria-hidden="true">
<tr>
<td width="139" scope="col" class="header">Widget 1</td>
<td width="94" scope="col" class="header">Widget 2</td>
<td width="94" scope="col" class="header">Widget 3</td>
<td width="114" scope="col" class="header">Widget 4</td>
</tr>
</table>
<table width="469" border="1">
<tr class="hideme">
<th width="139" class="noborder" scope="col" > <span class="offscreen">Ghost heading 1</span></th>
<th width="93" class="noborder" scope="col"><span class="offscreen" >Ghost heading 2</span></th>
<th width="94" class="noborder" scope="col"><span class="offscreen">Ghost heading 3</span></th>
<th width="115" class="noborder" scope="col"><span class="offscreen">Ghost heading 4</span></th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>5</td>
<td>6</td>
<td>7</td>
<td>8</td>
</tr>
<tr>
<td>99</td>
<td>9</td>
<td>0</td>
<td>11</td>
</tr>
</table>
JAWS | NVDA | VoiceOver | |
---|---|---|---|
FireFox32 | OK | OK | N/A |
IE11 | OK | OK | N/A |
Chrome 37 | OK | OK | N/A |
Safari MacOS | N/A | N/A | OK |
This is fairly elegant and reads in all modern screen readers