skip to content

You write `<table><tr><td>1</td></tr></table>` with no `<tbody>` in the source. What does the parsed DOM contain that your markup does not, and why?

level: middleimportance: should knowfreq 45%

answer

  1. the DOM holds an element you never typed
  2. rows need a row group
  3. in-table mode implies the missing one
  4. header and footer groups are not implied
  5. stray text lands outside the table

basics

~20 s

The parser inserts a tbody element for you, so the DOM is table > tbody > tr > td. A row start tag inside a table implies a tbody start tag, which means table rows are never direct children of the table element.

solid answer

~50 s

HTML's table model puts rows inside a row group, so the parser supplies the missing one. In the *in table* insertion mode, a `<tr>` start tag is handled by acting as though a `<tbody>` start tag had been seen, inserting that element and then reprocessing the row inside it. Your DOM is therefore `table > tbody > tr > td`, even though `tbody` never appears in the source. Only `tbody` is implied — `thead` and `tfoot` are never inserted for you, so if you want them you must write them. The practical consequence is that anything written against `table > tr`, whether a selector or a traversal, matches nothing, and that people conclude the browser "added a random element". It did not: it applied a specified rule. The same insertion mode also foster-parents stray text out of the table entirely.

code

html · 6 lines
html
<table><td>1</table>

<table>
  <thead><tr><th scope="col">Name</th></tr></thead>
  <tbody><tr><td>Ada</td></tr></tbody>
</table>

go deeper

for a junior

Remember that rows live inside a row group: the DOM is table, tbody, tr, td, even when you wrote no tbody, so address rows accordingly.

for a middle

Explain the in-table insertion mode: a tr start tag acts as though a tbody start tag had been seen and then reprocesses the row, while thead and tfoot are never implied.

for a senior

Bring in the failure modes you have actually hit — foster parenting pushing stray content above the table, and generated row markup whose imbalance moves content out of the tree.

for a principal

Own the generation strategy: whether table markup is assembled by string concatenation at all, how header semantics are guaranteed rather than hoped for, and where that is validated in the pipeline.

## What you wrote and what you get Source: ```html <table> <tr><td>1</td></tr> </table> ``` DOM: ```html <table> <tbody> <tr><td>1</td></tr> </tbody> </table> ``` The `tbody` is not a rendering artefact and not a devtools display convenience — it is a real element node in the document. ## Why: the in-table insertion mode HTML's tree construction is a state machine over *insertion modes*. Seeing `<table>` switches the parser into the **in table** mode, whose rules are unusually strict because tables have a rigid structure: a table contains an optional caption, optional column groups, and then row groups (`thead`, `tbody`, `tfoot`); rows live inside a row group; cells live inside rows. In that mode, a start tag named `tr`, `td` or `th` is handled by acting as if a `<tbody>` start tag had been seen — the parser inserts the row group, switches to the **in table body** mode, and then reprocesses the original token there. If the token was `<td>`, a `<tr>` gets implied too, on the same principle. That is how this parses correctly: ```html <table><td>1</table> ``` producing `table > tbody > tr > td`. ## Only tbody is implied This is the detail that separates a memorised fact from understanding. The parser will invent a `tbody` because it needs *some* row group to hold a row. It will never invent a `thead` or a `tfoot`, because those carry meaning the parser cannot infer — which rows are headers is an authoring decision. If your first row is a header row, you must write it: ```html <table> <thead><tr><th scope="col">Name</th></tr></thead> <tbody><tr><td>Ada</td></tr></tbody> </table> ``` End tags are on the same footing: `</td>`, `</th>`, `</tr>`, `</tbody>`, `</thead>` and `</tfoot>` are all optional, implied by the next cell, row, section, or by `</table>`. ## Foster parenting: the other in-table surprise The in-table mode also decides what to do with content that has no legal place in a table. Text that is not whitespace, and most start tags that are not table-structural, are **foster parented**: the parser inserts them into the DOM *immediately before the table element*, not inside it. ```html <table>Oops<tr><td>1</td></tr></table> ``` produces a text node `Oops` as a sibling preceding the table, then the table with its implied tbody. This is why a stray character or an accidental `<div>` inside table markup appears above the table on screen — the content did not move visually, it moved in the tree. ## Why this matters beyond trivia - **Anything addressing rows must account for the row group.** A rule or a query written as `table > tr` matches nothing on a normal table; `table tr`, or naming `tbody` explicitly, does. - **Generated markup must be balanced in the right place.** Template code that concatenates rows into a table string is one stray character away from foster parenting half a page above the table. - **Structure carries accessibility meaning.** Header rows only announce as headers when they are written as `thead` and `th`; no parser repair supplies that for you. ## Interview framing A good answer states the DOM shape, names the implied `tbody`, and immediately adds the boundary: `thead` and `tfoot` are never implied. Adding foster parenting as the second half of the in-table story is the detail that shows you know *why* tables have their own insertion mode at all — they are the part of HTML where the parser is least willing to accept arbitrary nesting.

  • Where does the parser put plain text written directly inside `<table>` but outside any cell?
    It is foster parented: the text node is inserted immediately before the table element, as its previous sibling. The in-table insertion mode has no legal place for non-whitespace characters, and the specified recovery relocates them out of the table rather than dropping them. It is why a stray character shows up above a table.
  • Do `</td>` and `</tr>` end tags have to be written at all?
    No. Cell and row end tags are optional: the next `<td>`, `<th>`, `<tr>`, a row-group end tag, or `</table>` implies them. Writing them is a readability and tooling choice rather than a correctness one. What you cannot omit is a `thead` or `tfoot` you actually want, since the parser never implies those.
  • Why does a rule or query written for `table > tr` match nothing on a normal table?
    Because the row is not a child of the table — it is a child of the implied or explicit row group, so the direct-child relationship is `tbody > tr`. Addressing rows with a descendant relationship, or naming the row group explicitly, works regardless of whether the section was written in the source.

saying these in an interview costs you the question

  • Thinks the tbody only appears once JavaScript touches the table
  • Believes thead is inserted implicitly like tbody
  • Claims tr is a valid direct child of table in the DOM
  • Says stray text inside a table renders in the first cell
  • Assumes devtools is displaying a fake wrapper element

context