Here is an example of a well-formatted <table>
, from the HTML specification:
<table>
<thead>
<tr> <th> ID <th> Measurement <th> Average <th> Maximum
<tbody>
<tr> <td> <th scope=rowgroup> Cats <td> <td>
<tr> <td> 93 <th> Legs <td> 3.5 <td> 4
<tr> <td> 10 <th> Tails <td> 1 <td> 1
<tbody>
<tr> <td> <th scope=rowgroup> English speakers <td> <td>
<tr> <td> 32 <th> Legs <td> 2.67 <td> 4
<tr> <td> 35 <th> Tails <td> 0.33 <td> 1
</table>

As you can see, a table can have multiple <tbody>
elements corresponding to multiple row groups. And headers with scope="rowgroup"
provide a label for the remaining row group.
For a well-formatted and accessible table, we can provide column and row headers as usual. More importantly, we can mark up each "Account" section as a <tbody>
, and provide a row group header for each section:
table {
border-collapse: collapse;
}
th, td {
padding: .5rem 1.25rem;
border: 1px solid black;
}
thead th {
background-color: #d3deea;
}
tbody th {
text-align: start;
}
tbody > tr:first-child > th {
background-color: #a7e3e4;
}
<table>
<thead>
<tr>
<th>Name</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
</table>
However, we cannot naïvely make the cells sticky, because the borders would remain in place:
/* Sticky */
thead > tr > th {
position: sticky;
top: 0;
}
tbody > tr:first-child > th {
position: sticky;
top: calc(1rem + 1lh + 1px); /* Offset by height of `thead > tr` */
}
/* Styling */
table {
border-collapse: collapse;
}
th, td {
padding: .5rem 1.25rem;
border: 1px solid black;
}
thead th {
background-color: #d3deea;
}
tbody th {
text-align: start;
}
tbody > tr:first-child > th {
background-color: #a7e3e4;
}
<table>
<thead>
<tr>
<th>Name</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 123</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 456</th>
</tr>
<tr>
<td>Trx 2</td>
<td>30</td>
</tr>
<tr>
<td>Trx 3</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
</table>
To "fix" the border issue, absolutely position a "border" on top. A simple solution would be to use ::after
pseudo-elements for the border:
/* Sticky */
thead > tr > th {
position: sticky;
top: 0;
}
tbody > tr:first-child > th {
position: sticky;
top: calc(1rem + 1lh + 1px); /* Offset by height of `thead > tr` */
}
/* Border of sticky headers */
thead > tr > th::after,
tbody > tr:first-child > th::after {
content: "";
position: absolute;
top: -1px;
left: -1px;
width: 100%;
height: 100%;
border: 1px solid black;
display: block;
pointer-events: none;
}
/* Styling */
table {
border-collapse: collapse;
}
th, td {
padding: .5rem 1.25rem;
border: 1px solid black;
}
thead th {
background-color: #d3deea;
}
tbody th {
text-align: start;
}
tbody > tr:first-child > th {
background-color: #a7e3e4;
}
<table>
<thead>
<tr>
<th>Name</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 12</th>
</tr>
<tr>
<td>Trx 1</td>
<td>20</td>
</tr>
<tr>
<td>Trx 2</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 34</th>
</tr>
<tr>
<td>Trx 3</td>
<td>30</td>
</tr>
<tr>
<td>Trx 4</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 56</th>
</tr>
<tr>
<td>Trx 5</td>
<td>20</td>
</tr>
<tr>
<td>Trx 6</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 78</th>
</tr>
<tr>
<td>Trx 7</td>
<td>30</td>
</tr>
<tr>
<td>Trx 8</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 910</th>
</tr>
<tr>
<td>Trx 9</td>
<td>20</td>
</tr>
<tr>
<td>Trx 10</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 1112</th>
</tr>
<tr>
<td>Trx 11</td>
<td>30</td>
</tr>
<tr>
<td>Trx 12</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 1314</th>
</tr>
<tr>
<td>Trx 13</td>
<td>20</td>
</tr>
<tr>
<td>Trx 14</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 1516</th>
</tr>
<tr>
<td>Trx 15</td>
<td>30</td>
</tr>
<tr>
<td>Trx 16</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 1718</th>
</tr>
<tr>
<td>Trx 17</td>
<td>20</td>
</tr>
<tr>
<td>Trx 18</td>
<td>15</td>
</tr>
<tr>
<th>Total</th>
<td>35</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="2" scope="rowgroup">Account 1920</th>
</tr>
<tr>
<td>Trx 19</td>
<td>30</td>
</tr>
<tr>
<td>Trx 20</td>
<td>20</td>
</tr>
<tr>
<th>Total</th>
<td>50</td>
</tr>
</tbody>
</table>
Note: Without pointer-events: none
, the ::after
pseudo-element—"extending" its parent cell's area—would catch pointer events, preventing the events from reaching the cell's children such as buttons.