Search code examples
htmlhtml-tableaccessibility

Colgroup for complex table with three levels of headers


Given a table like this: enter image description here

(HTML-code):

<table>
  <thead>
    <tr>
      <th>H1-A</th>
      <th colspan="3">H1-B</th>
      <th colspan="3">H1-C</th>
    </tr>
    <tr>
      <th>H2-A</th>
      <th colspan="2">H2-B</th>
      <th>H2-C</th>
      <th colspan="3">H2-D</th>
      <th colspan="3">H2-E</th>
    </tr>
    <tr>
      <th height="10">H3-A</th>
      <th>H3-B</th>
      <th>H3-C</th>
      <th>H3-D</th>
      <th>H3-E</th>
      <th>H3-F</th>
      <th>H3-G</th>
      <th>H3-I</th>
      <th>H3-J</th>
      <th>H3-K</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
    </tr>
    <tr>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
    </tr>
    <tr>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
      <td>data</td>
    </tr>
  </tbody>
</table>


Is it possible to use the colgroup-tag to mark the headers this table? Put another way:

Can colgroups be used for multi-row headers?

I need to mark the headers for accesibility purposes and it seems like this is the only option for such tables, given that the headers-attribute seems to have poor screenreader-support


Solution

  • This isn't as straightforward as I thought it would be, I tried numerous techniques such as just typical colgroup and then I even tried the headers attribute, which allows you to define headers via an ID.

    After a bit of searching I came across this example page where someone had tried 3 level column headers (example 3) and they experienced similar issues.

    After a bit of hacking about I did manage to get something that is workable and seems OK but it would need a lot more testing in other screen readers as I only have NVDA and JAWS on my PC (and don't have time to test all the browser-screen reader combinations at the moment).

    A possible solution

    As with anything if I can't get semantic elements to work and can't get WAI-ARIA to work then I fall back to our old friends aria-hidden and visually hidden text.

    What I do is completely hide the first two header rows in the table from screen readers and then use visually hidden text to add the information back into the 3rd level headers.

    So for example for column 10 I add "H1-C, H2-E" before "H3-K".

    <th scole="col"><span class="visually-hidden">H1-C, H2-E,</span>H3-K</th>

    This way a screen reader user is effectively presented with a normal 10 column table which is easy to navigate, but all the relevant header information is read out.

    For example the first cell is read as

    row 2 H1-A, H2-A,H3-A column 1 data 1a

    You would obviously want to build some solution (preferably on the server but with JS would be acceptable in a pinch) that does this automatically to avoid any mistakes being made.

    It is not an ideal solution, but it is the best I can come up with and is what I would do as at least all the relevant information is presented to screen reader users.

    Caveat: I would only use the solution if I could automate the process, if I could not automate generation of the visually hidden text for each column I would not do anything and leave the table as it is, due to the fact that a mistake here would likely be less accessible than just leaving the table alone.

    Example

    table{
        width: 100%;
    }
    th{
       text-align: center;
    }
    th, td{
        border: 1px solid #333;
    }
    
    .visually-hidden { 
        border: 0;
        padding: 0;
        margin: 0;
        position: absolute !important;
        height: 1px; 
        width: 1px;
        overflow: hidden;
        clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
        clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
        clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
        white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
    }
    <table>
      <thead>
        <tr aria-hidden="true">
          <th colspan="4">H1-A</th>
          <th colspan="3">H1-B</th>
          <th colspan="3">H1-C</th>
        </tr>
        <tr aria-hidden="true">
          <th>H2-A</th>
          <th colspan="2">H2-B</th>
          <th>H2-C</th>
          <th colspan="3">H2-D</th>
          <th colspan="3">H2-E</th>
        </tr>
        <tr>
          <th scole="col" height="10"><span class="visually-hidden">H1-A, H2-A,</span>H3-A</th>
          <th scole="col"><span class="visually-hidden">H1-A, H2-B,</span>H3-B</th>
          <th scole="col"><span class="visually-hidden">H1-A, H2-B,</span>H3-C</th>
          <th scole="col"><span class="visually-hidden">H1-A, H2-C,</span>H3-D</th>
          <th scole="col"><span class="visually-hidden">H1-B, H2-D,</span>H3-E</th>
          <th scole="col"><span class="visually-hidden">H1-B, H2-D,</span>H3-F</th>
          <th scole="col"><span class="visually-hidden">H1-B, H2-D,</span>H3-G</th>
          <th scole="col"><span class="visually-hidden">H1-C, H2-E,</span>H3-I</th>
          <th scole="col"><span class="visually-hidden">H1-C, H2-E,</span>H3-J</th>
          <th scole="col"><span class="visually-hidden">H1-C, H2-E,</span>H3-K</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>data 1a</td>
          <td>data 2a</td>
          <td>data 3a</td>
          <td>data 4a</td>
          <td>data 5a</td>
          <td>data 6a</td>
          <td>data 7a</td>
          <td>data 8a</td>
          <td>data 9a</td>
          <td>data 10a</td>
        </tr>
        <tr>
          <td>data 1b</td>
          <td>data 2b</td>
          <td>data 3b</td>
          <td>data 4b</td>
          <td>data 5b</td>
          <td>data 6b</td>
          <td>data 7b</td>
          <td>data 8b</td>
          <td>data 9b</td>
          <td>data 10b</td>
        </tr>
      </tbody>
    </table>