<tbody> Table body
Wraps the main rows of data in a table.
<tbody> wraps the main data rows. Use more than one to split a table's rows into sections.
Rows written straight into a table get a tbody added by the browser. CSS feels this: table > tr matches nothing, because the rows now sit inside that invisible tbody. Write tbody yourself and the surprise never happens.
Why the parser inserts it on its own is explained in the tbody element definition (opens in a new tab). You can practise table structure in the Table Basics lesson.
Syntax
<table>
<tbody>
<tr>
<td>Data</td>
</tr>
</tbody>
</table>When to use it
You get a <tbody> in every table whether you type one or not. So the interesting decision is when to write several. Do it when the rows fall into natural sections: starters, mains and desserts on a menu, a bank statement by month, a timetable with a block for each day. Every group can then carry its own row-group header with scope="rowgroup", plus its own border or background.
Sections with different columns are really different data, and they belong in separate tables, each with a caption. If all you want is a gap between rows, add a CSS border. A tbody added only for spacing is noise.
Watch the zebra stripes. A selector like tr:nth-child(even) counts rows inside each tbody, since every row is a child of its own group. After a section with an odd number of rows, the pattern in the next section starts out of step. For the basic structure, see the Table Basics lesson.
Live example
Edit the code and watch the preview.
<table>
<thead>
<tr><th scope="col">Fruit</th><th scope="col">Stock</th></tr>
</thead>
<tbody>
<tr><td>Apples</td><td>12</td></tr>
<tr><td>Pears</td><td>7</td></tr>
</tbody>
</table>More examples
Menu grouped by course
One tbody per course. The first row of each holds a header cell with scope="rowgroup" spanning both columns, so a screen reader user going through the prices still hears which course a dish is in.
<table>
<caption>Lunch menu</caption>
<tbody>
<tr>
<th scope="rowgroup" colspan="2">Starters</th>
</tr>
<tr><td>Tomato soup</td><td>$5</td></tr>
<tr><td>Garlic bread</td><td>$4</td></tr>
</tbody>
<tbody>
<tr>
<th scope="rowgroup" colspan="2">Mains</th>
</tr>
<tr><td>Mushroom risotto</td><td>$12</td></tr>
<tr><td>Fish and chips</td><td>$14</td></tr>
</tbody>
</table>Greying out finished tasks
Open tasks and finished tasks sit in two separate tbody elements. One class on the second group greys out every finished row in one go. The Status column still spells out Done, so color is never the only signal.
<style>
.archived{color:#666;background:#f3f3f3}
</style>
<table>
<thead>
<tr><th scope="col">Task</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr><td>Write report</td><td>Open</td></tr>
<tr><td>Book venue</td><td>Open</td></tr>
</tbody>
<tbody class="archived">
<tr><td>Send invites</td><td>Done</td></tr>
<tr><td>Order food</td><td>Done</td></tr>
</tbody>
</table>Tip: Leave tbody out and the browser adds it anyway.
Browser support
tbody works in all browsers and belongs to the HTML Living Standard. Write rows straight inside a table and every browser adds an implied tbody for you, so the DOM ends up identical. Multiple tbody elements in one table are valid, and they render the same everywhere.
Accessibility
tbody adds no announcement of its own. Clear row groups do help you keep headers and data consistent, and the W3C multi-level headers tutorial (opens in a new tab) has a table with several row groups.
Common mistakes
Wrong
table > tr { background: #eee; }Right
table > tbody > tr { background: #eee; }The browser always adds a tbody, so a tr never ends up as a direct child of table.
Frequently asked questions
Why does tbody appear in DevTools when I did not write it?
Can a table have more than one tbody?
Yes. Several <tbody> elements split a table into row groups, such as one per month or per department. Each group can open with a row whose header cell has scope="rowgroup". thead and tfoot are limited to one each.