HTML Tables: Styling & Accessibility Best Practices
Build accessible, responsive HTML tables. Semantic markup, CSS styling, sticky headers, responsive patterns, screen reader support, and WCAG compliance.
- Build accessible, responsive HTML tables.
- Covers semantic table markup.
- Covers table accessibility (wcag).
- Covers css styling techniques.
- Covers sticky headers.
Semantic Table Markup
A well-structured HTML table uses semantic elements that convey meaning to browsers and assistive technologies:
<caption> provides a visible title and accessible label. <thead>, <tbody>, and <tfoot> group rows by function. <th> with scope attributes tells screen readers which cells are headers and what direction they apply.
Table Accessibility (WCAG)
Tables are one of the trickiest elements for accessibility. Screen readers navigate tables cell-by-cell, announcing the associated header for each data cell. Without proper markup, the data becomes an incomprehensible grid of values.
gap instead of margin hacks for flexbox spacing. It keeps layout code cleaner and is supported in all modern browsers.Always use <th> with scope="col" or scope="row". This explicitly associates header cells with their data columns or rows. Without scope, screen readers guess โ and often guess wrong in complex tables.
Add a <caption> element. It serves as both a visible title and the accessible name for the table. If you want it visually hidden, use CSS rather than removing it.
Avoid merged cells when possible. colspan and rowspan create complex relationships that are hard for screen readers to navigate. If you must merge cells, use headers attributes to explicitly associate data cells with their headers.
Never use tables for layout. Use CSS Grid or Flexbox instead. Layout tables confuse screen readers and violate WCAG guidelines.
CSS Styling Techniques
Modern CSS makes tables look polished without sacrificing semantics:
width, height, top, or left. These trigger layout recalculations on every frame and can drop performance below 60fps.border-collapse: collapse removes the gap between cell borders. Use padding instead of cellpadding (an obsolete HTML attribute). Align numeric data to the right with text-align: right for easier comparison.
Sticky Headers
For long tables that scroll, sticky headers keep column labels visible:
The header row stays fixed at the top of the scrollable area while the body scrolls beneath it. Add a subtle box-shadow to the sticky header to create visual separation from the content below.
For tables inside scrollable containers (not the page itself), apply position: sticky relative to the scrollable parent. The key is that the table's overflow container determines the sticky boundary.
Responsive Table Patterns
Wide tables break on mobile screens. Several patterns handle this:
Horizontal scroll: Wrap the table in a div with overflow-x: auto. The simplest approach โ the table stays intact and users swipe to see hidden columns.
Card layout: At narrow viewports, hide the table structure and reformat each row as a stacked card using CSS. Each cell is labeled with its column header via data-label attributes and ::before pseudo-elements.
Priority columns: Hide less important columns on mobile with display: none in media queries. Show a "View all" button that reveals the full table.
The horizontal scroll pattern is the most reliable and accessible โ it doesn't alter the table's semantic structure, which keeps screen readers working correctly.
Zebra Striping & Hover States
Alternating row backgrounds improve readability in dense tables:
Keep the contrast between alternating rows subtle โ too much contrast is distracting. For dark themes, use very low opacity whites (2โ4%). For light themes, use very low opacity grays.
Hover states help users track across wide tables. A highlighted row or a cursor-following cell highlight provides visual context for which data point the user is examining.
Sorting & Filtering
Interactive tables often need sorting and filtering. For client-side sorting, JavaScript reorders the <tbody> rows based on the clicked column header. Add ARIA attributes to communicate sort state:
Valid aria-sort values are ascending, descending, and none. Update this attribute when the sort changes so screen reader users know the current sort order.
For large datasets, server-side sorting and pagination are more performant. The Table Generator tool creates the base HTML structure โ from there, add sorting with vanilla JavaScript or a library like AG Grid or TanStack Table.
When Not to Use Tables
Tables are for tabular data โ information that has a meaningful relationship between rows and columns. They are not for layout, form alignment, image galleries, or navigation.
If your "table" is really a list of cards (each with a title, description, and link), use a CSS Grid or Flexbox layout. If it's a comparison (product A vs product B), a table might be appropriate โ but consider whether a side-by-side card layout would be clearer.
The test: if removing the table headers makes the data meaningless, it belongs in a table. If the data makes sense without headers (like a gallery or a list of links), use a different element.