Multi-level headers
Designing with multi-level headers
A table with multi-level headers has headers stacked three deep, or column headers that repeat or change partway down a table.
To associate multi-level headers with data cells,
- Give each header a unique
id
attribute value. - Reference those
id
attribute values in theheaders
attribute of data cells.
Using the id/headers
attributes technique is a last resort, as it's not currently (2021) well-supported by assistive technologies. While the “good” example table below is technically compliant, it performs poorly in screen readers.
Important: Whenever possible, simplify the data presentation by breaking a complex table into two or more simpler tables.
A table summary may be necessary. Column headers that repeat or change partway down a table can easily surprise screen readers users as they descend rows. The same is true of column-spanning row headers. In the table summary, mention their presence.
Good example: Table with headers three deep
In this example, the row headers are three deep, requiring the use of id/headers
markup to associate the data cells with their headers. The WET Table Validator provides the id/headers
markup. A visually-hidden table description is nested in the <caption>
element.
Example begins
Type of Vehicle | Country | Term of Lease | Policy Requirement |
---|---|---|---|
executive | CANADA | long-term |
Comprehensive commercial insurance, including collision and third party liability; - self-underwrite the deductible - |
short-term |
Comprehensive commercial insurance, including collision and third party liability; - self-underwrite the deductible - |
||
U.S. | long-term |
Comprehensive commercial insurance, including collision and third party liability; - self-underwrite the deductible - |
|
short-term |
Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks; - self-underwrite the deductibles - |
||
non-executive | CANADA | long-term |
Self-underwrite except if provincial legislation applies |
short-term |
Comprehensive commercial insurance, including collision and third party liability; - self-underwrite the deductible - |
||
U.S. | long-term |
Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks; - self-underwrite any damage to government vehicle - |
|
short-term |
Utilize commercial insurance coverage (third party liability and collision for the U.S. risks) administered by Services and Specialized Acquisitions Management Sector, PWGSC; - self-underwrite the deductible - |
Example ends
View HTML snippet - Assigning id and headers attributes
Code begins
<tr>
<th id="tbl6">Type of Vehicle</th>
<th id="tbl7">Country</th>
<th id="tbl8">Term of Lease</th>
<th id="tbl9">Policy Requirement</th>
</tr>
<tr>
<th rowspan="4" id="tbl12" headers="tbl6">executive</th>
<th rowspan="2" id="tbl13" headers="tbl7 tbl12">CANADA</th>
<th id="tbl14" headers="tbl8 tbl12 tbl13">long-term</th>
<td headers="tbl9 tbl12 tbl13 tbl14">
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
Code ends
View complete HTML
Code begins
<table>
<caption>
Vehicles leased by the Government
<span class="wb-inv">Column 1 lists executive and non-executive vehicles. Column 2 lists the country (Canada or the USA). Column 3 lists the term of lease (long-term or short term). Column 4 lists the policy requirement.</span>
</caption>
<tbody>
<tr>
<th id="tbl6">Type of Vehicle</th>
<th id="tbl7">Country</th>
<th id="tbl8">Term of Lease</th>
<th id="tbl9">Policy Requirement</th>
</tr>
<tr>
<th rowspan="4" id="tbl12" headers="tbl6">executive</th>
<th rowspan="2" id="tbl13" headers="tbl7 tbl12">CANADA</th>
<th id="tbl14" headers="tbl8 tbl12 tbl13">long-term</th>
<td headers="tbl9 tbl12 tbl13 tbl14">
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
<tr>
<th id="tbl25" headers="tbl8">short-term</th>
<td headers="tbl9 tbl12 tbl13 tbl25">
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
<tr>
<th rowspan="2" id="tbl30" headers="tbl7">U.S.</th>
<th id="tbl31" headers="tbl8 tbl30">long-term</th>
<td headers="tbl9 tbl12 tbl30 tbl31">
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
<tr>
<th id="tbl36" headers="tbl8">short-term</th>
<td headers="tbl9 tbl12 tbl30 tbl36">
<p>Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks;</p>
<p>- self-underwrite the deductibles -</p>
</td>
</tr>
<tr>
<th rowspan="4" id="tbl41" headers="tbl6">non-executive</th>
<th rowspan="2" id="tbl42" headers="tbl7 tbl41">CANADA
</th><th id="tbl43" headers="tbl8 tbl41 tbl42">long-term</th>
<td headers="tbl9 tbl41 tbl42 tbl43">
<p>Self-underwrite except if provincial legislation applies</p>
</td>
</tr>
<tr>
<th id="tbl48" headers="tbl8">short-term</th>
<td headers="tbl9 tbl41 tbl42 tbl48">
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
<tr>
<th rowspan="2" id="tbl53" headers="tbl7">U.S.</th>
<th id="tbl54" headers="tbl8 tbl53">long-term</th>
<td headers="tbl9 tbl41 tbl53 tbl54">
<p>Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks;</p>
<p>- self-underwrite any damage to government vehicle -</p>
</td>
</tr>
<tr>
<th id="tbl59" headers="tbl8">short-term</th>
<td headers="tbl9 tbl41 tbl53 tbl59">
<p>Utilize commercial insurance coverage (third party liability and collision for the U.S. risks) administered by Services and Specialized Acquisitions Management Sector, <abbr title="Public Works and Government Services Canada">PWGSC</abbr>;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
</tbody>
</table>
Code ends
Good example: Multi-level table simplified
The table in the previous example is overly complex, requiring the poorly-supported id/headers
technique. A better approach is to split a complex table into two or more smaller tables. In this example, the “Type of Vehicle" (executive or non-executive) is moved from column 1 to the table <caption>
, resulting in one table about executive vehicles and a second table about non-executive vehicles. The simpler tables consist of two levels of headers, so can be rendered using the scope
attribute rather than with id/headers
attributes. The scope
method is easier to author and maintain, and is better supported by user agents than the id/headers
method.
Example begins
Country | Term of Lease | Policy Requirement |
---|---|---|
CANADA | long-term |
Self-underwrite except if provincial legislation applies |
short-term |
Comprehensive commercial insurance, including collision and third party liability; - self-underwrite the deductible - |
|
U.S. | long-term |
Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks; - self-underwrite any damage to government vehicle - |
short-term |
Utilize commercial insurance coverage (third party liability and collision for the U.S. risks) administered by Services and Specialized Acquisitions Management Sector, PWGSC; - self-underwrite the deductible - |
Example ends
View HTML
Code begins
<table>
<caption>Non-executive Vehicles leased by the Government</caption>
<thead>
<tr>
<th scope="col">Country</th>
<th scope="col">Term of Lease</th>
<th scope="col">Policy Requirement</th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">CANADA</th>
<th scope="row">long-term</th>
<td>
<p>Self-underwrite except if provincial legislation applies</p>
</td>
</tr>
<tr>
<th scope="row">short-term</th>
<td>
<p>Comprehensive commercial insurance, including collision and third party liability;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
</tbody>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">U.S.</th>
<th scope="row">long-term</th>
<td>
<p>Purchase additional commercial insurance to cover third party liability and collision for the U.S. risks;</p>
<p>- self-underwrite any damage to government vehicle -</p>
</td>
</tr>
<tr>
<th scope="row">short-term</th>
<td>
<p>Utilize commercial insurance coverage (third party liability and collision for the U.S. risks) administered by Services and Specialized Acquisitions Management Sector, <abbr title="Public Works and Government Services Canada">PWGSC</abbr>;</p>
<p>- self-underwrite the deductible -</p>
</td>
</tr>
</tbody>
</table>
Code ends
Good example: Table with three headers related to each data cell
In this example, the table headers “Organic (1kg)" and “Non-organic (1kg)" serve as subheadings to describe the next section of the table. Using the headers
attribute, all three headers are properly associated with the data cell.
This example sets the table in a <figure>
element, as described in the section Caption & summary. Rather than use a <caption>
element, the <figcaption>
holds the table title, in a <strong>
element, as well as a table summary to aid comprehension. The aria-labelledby
and aria-describedby
attributes on the <table>
element point to the title and summary, respectively, which makes the associations stronger and more reliable in assistive technologies.
Note that the empty header cell is given an id
attribute value and a non-breaking space as content. The cell is referenced by top-level headers in their headers
attribute. This prevents some assistive technologies from declaring the cell.
Example begins
Column one lists organic produce followed by non-organic, other columns show the cost by country.
Canada | USA | UK | |
---|---|---|---|
Organic (1kg) | |||
Apples | $3.62 | $4.87 | $2.69 |
Bananas | $1.47 | $1.68 | $1.60 |
Onions | $2.28 | $2.81 | $1.44 |
Non-organic (1kg) | |||
Apples | $3.37 | $4.53 | $2.50 |
Bananas | $1.37 | $1.56 | $1.49 |
Onions | $2.12 | $2.61 | $1.34 |
Example ends
View HTML snippet - Assigning id attributes to <th> cells
Code begins
<thead>
<tr>
<td id="blank"> </td>
<th id="can" scope="col" headers="blank">Canada</th>
<th id="usa" scope="col" headers="blank">USA</th>
<th id="uk" scope="col" headers="blank">UK</th>
</tr>
<thead>
<tbody>
<tr>
<th id="org" class="span" colspan="4" scope="colgroup" headers="blank">
Organic (1kg)
</th>
</tr>
<tr>
<th id="o-apples" headers="org">
Apples
</th>
[…]
</tr>
[…]
</tbody>
View HTML snippet - Assigning headers attributes to <td> cells
Code begins
[…]
<td headers="org o-apples can">$3.62</td>
<td headers="org o-apples usa">$4.87</td>
[…]
Code ends
View complete HTML
Code begins
<figure>
<figcaption>
<strong id="caption">Cost of organic vs non-organic produce in Canada, the USA and the UK</strong>
<p id="summary">Column one lists organic produce followed by non-organic, other columns show the cost by country.</p>
</figcaption>
<table aria-labelledby="caption" aria-describedby="summary">
<thead>
<tr>
<td id="blank"> </td>
<th headers="blank" id="can">Canada</th>
<th headers="blank" id="usa">USA</th>
<th headers="blank" id="uk">UK</th>
</tr>
</thead>
<tbody>
<tr>
<th headers="blank" id="org" class="span" colspan="4">Organic</th>
</tr>
<tr>
<th headers="org" id="o-apples">Apples</th>
<td headers="org o-apples can">$3.62</td>
<td headers="org o-apples usa">$4.87</td>
<td headers="org o-apples uk">$2.69</td>
</tr>
<tr>
<th headers="org" id="o-bananas">Bananas</th>
<td headers="org o-bananas can"> $1.47</td>
<td headers="org o-bananas usa">$1.68</td>
<td headers="org o-bananas uk">$1.60</td>
</tr>
<tr>
<th headers="org" id="o-onions">Onions</th>
<td headers="org o-onions can">$2.28</td>
<td headers="org o-onions usa">$2.81</td>
<td headers="org o-onions uk">$1.44</td>
</tr>
<tr>
<th headers="blank" id="non-org" class="span" colspan="4">Non-organic</th>
</tr>
<tr>
<th id="n-apples" headers="non-org">Apples</th>
<td headers="non-org n-apples can">$3.37</td>
<td headers="non-org n-apples usa">$4.53</td>
<td headers="non-org n-apples uk">$2.50</td>
</tr>
<tr>
<th id="n-bananas" headers="non-org">Bananas</th>
<td headers="non-org n-bananas can">$1.37</td>
<td headers="non-org n-bananas usa">$1.56</td>
<td headers="non-org n-bananas uk">$1.49</td>
</tr>
<tr>
<th id="n-onions" headers="non-org">Onions</th>
<td headers="non-org n-onions can">$2.12</td>
<td headers="non-org n-onions usa">$2.61</td>
<td headers="non-org n-onions uk"> $1.34</td>
</tr>
</tbody>
</table>
</figure>
Code ends