Skip to content Skip to site navigation Skip to service navigation

Accessible Table with Fixed "Sticky" Header

Creating an accessible table with a sticky header can be accomplished in a couple of different ways. The way not to accomplish this is to separate the table into two different tables, one with the headers and one with the data cells. That breaks the header relationships and creates an inaccessible table. Instead, consider the following table:

CSS Table with sticky header
Col 1 Col 2 Col 3 Col 4
Row Header 1 Data 1 Data 2 Data 3
Row Header 2 Data 1 Data 2 Data 3
Row Header 3 Data 1 Data 2 Data 3
Row Header 4 Data 1 Data 2 Data 3
Row Header 5 Data 1 Data 2 Data 3
Row Header 6 Data 1 Data 2 Data 3
Row Header 7 Data 1 Data 2 Data 3
Row Header 8 Data 1 Data 2 Data 3
Row Header 9 Data 1 Data 2 Data 3
Row Header 10 Data 1 Data 2 Data 3
Row Header 11 Data 1 Data 2 Data 3
Row Header 12 Data 1 Data 2 Data 3
Row Header 13 Data 1 Data 2 Data 3
Row Header 14 Data 1 Data 2 Data 3
Row Header 15 Data 1 Data 2 Data 3
Row Header 16 Data 1 Data 2 Data 3
Row Header 17 Data 1 Data 2 Data 3
Row Header 18 Data 1 Data 2 Data 3
Row Header 19 Data 1 Data 2 Data 3
Row Header 20 Data 1 Data 2 Data 3
Row Header 21 Data 1 Data 2 Data 3
Row Header 22 Data 1 Data 2 Data 3
Row Header 23 Data 1 Data 2 Data 3
Row Header 24 Data 1 Data 2 Data 3

Code

In short, the best way to accomplish this task is to fix the THEAD with a position: sticky and the rest of the table with overflow. The table itself is built normally with two exceptions. First there is a wrapper around the table with a CSS class on it (which we will define next), and then there is a tabindex="0" on the table itself, which is necessary for support using the Chrome web browser.

<div class="fixTableHead">
   <table tabindex="0">
      <caption class="sr-only">CSS Table with sticky header</caption>
      <thead>
         <tr>
            <th scope="col">Col 1</th>
            <th scope="col">Col 2</th>
            <th scope="col">Col 3</th>
            <th scope="col">Col 4</th>
         </tr>
      </thead>
      <tbody>
         <tr>
            <th scope="row">Row Header 1</th>
            <td>Data 1</td>
            <td>Data 2</td>
            <td>Data 3</td>
         </tr>
        <!-- Insert More Rows Here -->
      </tbody>
   </table>
</div>

Then the CSS itself is pretty simple with just three declarations. The first for the wrapper defines how the region will overflow, the second then tells the thead row to be stuck in place. Finally the last declaration, adds visual focus to the table for the chrome users.

.fixTableHead {
   overflow-y: auto;
   height: 15em;
}

.fixTableHead thead th {
   position: sticky;
   top: 0;
}

table:focus {
   border: #f00 solid 2px !important;
}

There are additional variations on how to create a sticky table header, but this one is the simplest that we have seen.

Last modified May 4, 2023