Table headers must have visible text
Last updated:
Related Guides
Table headers must not be empty and require visible text
Every table header cell (th) needs visible, descriptive text.
This applies to column and row headers in data tables on web and mobile web.
It affects screen reader users, low-vision users, and anyone scanning data for meaning.
Why It Matters
Without visible header text, people cannot tell what a column or row represents. This creates confusion for sighted users and those with low vision or zoom who rely on visible labels.
Screen reader users depend on header associations to understand cell context. If a th is empty or labeled only via ARIA, the header may be skipped in some reading modes, causing data to lose meaning.
Cognitive load increases when users must guess the purpose of columns or rely on memorizing positions instead of reading clear labels.
Common Causes
- Empty
<th>elements left as placeholders. - Using only icons, SVGs, or background images in headers with no accompanying text.
- Adding
aria-labelor title to a header instead of visible text. - CSS that hides header text (display:none, visibility:hidden, clip, or off-screen techniques).
- Using
<td>for headers instead of<th>, breaking announced relationships. - Responsive layouts that drop or replace header text with icons at narrow widths.
How to Fix
- Put clear, visible text inside every
<th>.- Example labels: Name, Status, Due Date, Amount.
- Recommendation: keep labels short (1–3 words) and unambiguous.
- Use proper header semantics.
- Simple tables: use
<th scope="col">for column headers and<th scope="row">for row headers. - Complex tables: use headers/id to explicitly associate data cells with multiple headers.
- Simple tables: use
- Do not rely on ARIA-only names.
aria-labeloraria-labelledbydoes not replace visible header text. Use ARIA only as a supplement if needed.
- Ensure visibility across breakpoints.
- At narrow viewports and at 400% zoom, the header text must remain visible and not be replaced by icon-only UI.
- If space is tight, wrap text or use acceptable abbreviations. Provide a longer label in the table caption or nearby text if needed.
- Icon support (optional).
- If including an icon (e.g., sort indicator), keep the text visible. Mark decorative icons
aria-hidden="true".
- If including an icon (e.g., sort indicator), keep the text visible. Mark decorative icons
How to Test
Visual check
- Inspect each
<th>. Is there readable text visible on screen at normal and narrow widths? At 400% zoom?
Programmatic check
- Verify table headers use
<th>, not<td>. - Confirm
<th>elements are not empty and not hidden via CSS. - For simple tables, ensure appropriate scope attributes. For complex tables, verify headers/id mappings.
- Optional quick scan in the console:
const emptyHeaders = [...document.querySelectorAll('th')]
.filter(th => !th.textContent.trim());
console.log(emptyHeaders);
Screen reader check (NVDAJAWSVoiceOver)
- Navigate the table by cell. Move to a data cell and confirm the associated header text is announced.
- Move across columns and rows and verify the correct header is read each time.
Keyboard check (when headers contain controls)
- If headers include sortfilter buttons, ensure they receive focus, have visible text or an accessible name, and do not obscure the header label.Mobile/touch check
- In a narrow viewport, confirm header text remains visible and readable. No icon-only headers.
Good Example
<table>
<caption>Quarterly Revenue Report</caption>
<thead>
<tr>
<th scope="col">Quarter</th>
<th scope="col">Revenue</th>
<th scope="col">Change</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Q1</th>
<td>$250,000</td>
<td>+4%</td>
</tr>
<tr>
<th scope="row">Q2</th>
<td>$265,000</td>
<td>+6%</td>
</tr>
</tbody>
</table>Bad Example
<table>
<thead>
<tr>
<th></th> <!-- empty header -->
<th aria-label="Revenue"></th> <!-- ARIA-only, not visible -->
<th><span class="sr-only">Change</span><svg aria-hidden="true"></svg></th> <!-- no visible text -->
</tr>
</thead>
</table>Quick Checklist
- Each
<th>contains short, visible, descriptive text. - No empty headers and no icon-only headers.
- Do not rely on aria-label/title as the sole header name.
- Use
<th>(not<td>) for headers and apply scope or headers/id correctly. - Header text stays visible at narrow viewports and 400% zoom.
- Decorative icons in headers are
aria-hiddenand do not replace text. - Screen readers announce correct headers when navigating data cells.