How Do I Add Accessible Row Headers to a Generated HTML Table?

Share

The HTML Table Generator on this site can produce a caption and a row of column headers, but it does not add row headers on its own. To add row headers to a generated HTML table so each row’s first cell reads as a label instead of plain data, you need to edit the generated markup yourself. This guide covers which cells to change, when scope="col" belongs on your column headers, and where this edit stops being enough for a more complex table.

The distinction matters for assistive technology users. A table with only column headers announces what each column contains, but says nothing about what a given row represents once a reader moves past the header row. Row and column headers solve two different problems, and the fix below follows the two-header approach from W3C’s Web Accessibility Initiative (WAI) tutorials.

What the HTML Table Generator Actually Outputs

The generator’s “First row as header (th)” setting turns the first row’s cells into <th> elements, and its optional caption names the table as a whole. That covers the column-header direction and the table’s overall label, and it is a genuinely useful starting point.

What it does not do is add row headers. In the generator’s own published example, the first row becomes <th>, but every cell in every following row, including the first cell that usually names the row, stays a plain <td>. No scope attribute appears anywhere either. The tool builds a table; it does not check or fix accessibility for you, so treat it as a starting point rather than a validator.

This matters if you draft page content in the WYSIWYG HTML Editor, since that editor has no table tool of its own. The practical path is to design the table in the generator, then bring the markup into an HTML editor for the manual edits below.

Developer editing markup to add row headers to a generated HTML table

How to Add Row Headers to a Generated HTML Table

Once you have generated a table, the edit itself is small and mechanical. Follow these steps:

  1. Enter your data in the HTML Table Generator, enable “First row as header (th)” if the first row holds column labels, add a caption if useful, and generate the code.
  2. Open the generated markup in an HTML Code Editor so you can see and edit the raw tags with syntax highlighting.
  3. In each body row, change the first cell from <td>Label</td> to <th scope="row">Label</th>, but only where that cell genuinely identifies the row rather than holding ordinary data.
  4. If the table also has column headers, add scope="col" to the existing top-row <th> cells so both directions are explicit.
  5. Run the result through an HTML Beautifier if the edits leave the indentation messy, then review it once more before publishing.

The two blocks below are illustrative markup adapted from the pricing example on the generator’s page. They are not verbatim generator output, and the exact tags the tool emits may differ, for example in whether it wraps rows in <thead> and <tbody>. The first block shows a typical starting table with a header row and plain data cells. The second shows the same table after the row header edit.

<table>
  <caption>Hosting plans</caption>
  <thead>
    <tr>
      <th>Plan</th>
      <th>Storage</th>
      <th>Price per month</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Starter</td>
      <td>10 GB</td>
      <td>$4</td>
    </tr>
    <tr>
      <td>Pro</td>
      <td>100 GB</td>
      <td>$12</td>
    </tr>
  </tbody>
</table>
<table>
  <caption>Hosting plans</caption>
  <thead>
    <tr>
      <th scope="col">Plan</th>
      <th scope="col">Storage</th>
      <th scope="col">Price per month</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Starter</th>
      <td>10 GB</td>
      <td>$4</td>
    </tr>
    <tr>
      <th scope="row">Pro</th>
      <td>100 GB</td>
      <td>$12</td>
    </tr>
  </tbody>
</table>

Changing options or grid values in the generator regenerates its output, so make these edits after you finish shaping the data, and reapply them if you regenerate.

Row Headers vs Column Headers: Telling Them Apart

A column header labels the data beneath it down a column. A row header labels the data across a single row. The two do different jobs, which is why the WHATWG HTML Living Standard defines row and col as the two main values of the th element’s scope attribute rather than treating every th the same way.

Not every first-column cell deserves a scope="row" conversion. Convert a cell only when it genuinely names the row, such as a product name, a person’s name, or a date everything else in that row relates to. Leave an ordinary value as a plain <td>. Forcing every first-column value into a header when it is not acting as one can make the table harder to navigate, not easier.

WAI guidance also notes that a small, unambiguous table with only one header direction can use plain <th> cells without an explicit scope value, since the association can be worked out automatically. A short contact list is a typical example:

<table>
  <caption>Team contacts</caption>
  <tr>
    <th>Name</th>
    <th>Email</th>
  </tr>
  <tr>
    <td>Alex Rivera</td>
    <td>[email protected]</td>
  </tr>
</table>

Once a table has both row and column headers, or grows larger and less obvious, adding explicit scope values removes any ambiguity about which direction each header covers.

When You Need Both Row and Column Headers in a Generated HTML Table

When headers run in both directions and the top-left corner cell has no meaningful label of its own, I suggest leaving that cell as an empty <td> rather than an empty <th>. An empty header cell is a common accessibility-checker failure, and it gives assistive technology a header with nothing to announce. The following row is illustrative:

<tr>
  <td></td>
  <th scope="col">Q1</th>
  <th scope="col">Q2</th>
</tr>

If the corner does have a real label, such as “Plan” in the hosting example, keep it as a <th scope="col"> because it genuinely names the first column.

A caption does not substitute for this structure. It identifies the table as a whole, for example “Hosting plans,” but it does not tell assistive technology which cell is a column heading and which is a row heading. Keep the caption and the header cells doing their separate jobs.

A fully labeled hosting plans table ends up with three roles: the caption names the table, scope="col" on “Plan,” “Storage” and “Price per month” labels each column, and scope="row" on “Starter” and “Pro” labels each row.

Complex Tables and the Limits of This Edit

This th and scope edit fits a regular data table where headers apply cleanly to whole rows or columns. It is not a fix for every structure. If a header spans several rows or columns, you may need scope="rowgroup" or scope="colgroup" along with grouping markup that reflects the actual layout.

For multi-level or overlapping header relationships, simple scope values may not be enough. A more reliable approach is to give each header cell an id and reference those ids from the headers attribute on each data cell, or to redesign the table into a simpler shape. A wide, deeply nested table is often harder for readers and assistive technology alike than two smaller tables.

Whatever structure you end up with, review it after any edit. Regenerating, converting, or pasting the table elsewhere can strip manual changes, so check the markup again afterward and, where practical, test it with a screen reader rather than assuming the visual layout matches the underlying structure. This habit of rechecking output also helps you avoid other common HTML coding mistakes that only show up once you look at the actual tags rather than the rendered page.

Frequently asked questions

Does the HTML Table Generator create row headers automatically?

No. Its “First row as header (th)” setting only affects the first row, producing column headers. Every other row’s cells, including the first cell, are generated as plain td elements, so row headers have to be added by editing the markup afterward.

What is the difference between scope="row" and scope="col"?

scope=”col” tells assistive technology that a th cell labels the column beneath it, while scope=”row” tells it that a th cell labels the row it sits in. Both are defined values of the th element’s scope attribute in the HTML standard.

Should I convert every first-column cell to a th element?

No. Convert a first-column cell only when it genuinely identifies the row, such as a name or product label. If the cell just holds another piece of ordinary data, leave it as a td so the table structure stays accurate.

Do I need a scope attribute if my table only has column headers?

Not always. W3C’s WAI guidance notes that a small, unambiguous table with headers in only one direction can use plain th cells without an explicit scope value, since the association is usually clear. Adding scope is still helpful for larger or more complex tables.

What should go in the top-left corner cell of a two-header table?

If that cell has no meaningful label of its own, leave it as an empty td rather than an empty th, since empty header cells are a common accessibility-checker failure. If the cell does hold a real label, such as Plan for the first column, use a th with scope=”col”.

Does adding a caption fix missing row headers?

No. A caption names the table as a whole; it does not tell assistive technology which cells are row headers or column headers. Captions and header cells serve separate, non-interchangeable purposes.

How do I handle a table with merged or grouped headers?

Simple scope=”row” and scope=”col” values work for regular tables, but spanning headers may need scope=”rowgroup” or scope=”colgroup”, and multi-level relationships may require id and headers attributes or a simpler table redesign.

Next steps

The HTML Table Generator gives you a caption and column headers in one step, but to add row headers to a generated HTML table you still need to open the markup and change the relevant first-column cells from <td> to <th scope="row">, adding scope="col" to the existing header row when both directions apply. Convert only the cells that genuinely label their row, and leave the top-left corner cell empty when it has no real heading of its own.

For anything beyond a straightforward data table, spanning headers, grouped columns, or multi-level relationships, treat this edit as a foundation rather than a complete accessibility solution, and check the final structure before you publish it.

Related tools and resources on HTML Editor Online:

Sources and further reading

Read more

More developer guides