HTML table generator

Build clean, semantic table markup with accessible headers and a live preview — plus a starter stylesheet to make it look right.

HTML Table Generator

Build clean, semantic table markup — with a proper header row and accessible scope attributes — plus a starter stylesheet to make it look right.

Live Preview (markup + starter CSS, as it renders on a plain page)

Copy-Paste Output

HTML markup
Starter CSS

How to use this tool

Sighted readers understand a table instantly because they can see the grid. Screen reader users can't — they navigate cell by cell, and rely entirely on the markup to know what each value means.

That's what <thead>, <th> and the scope attribute provide. With them, moving through a table announces "Revenue, Q3: £48,000" — the header travels with the value. Without them, it announces "48,000" and the user has to remember which column they're in. On a wide table, that's the difference between usable and useless.

scope="col" marks a header for its column; scope="row" marks one for its row. Adding a <caption> is worth doing too — it gives the whole table a title that assistive technology announces on entry.

The layout-tables myth, and its modern inversion

In the 1990s, tables were the only reliable way to build page layouts, and the practice persisted long after CSS could do the job properly. The correction — "never use tables" — became received wisdom.

It over-corrected. The rule was always don't use tables for layout, not don't use tables. Today the more common mistake is the reverse: building tabular data out of divs and flexbox because tables feel outdated. That throws away every accessibility benefit above, plus keyboard navigation and the browser's own column alignment, and gains nothing — CSS can style a real <table> however you like.

The test is simple: if your content is rows and columns of related values, it is a table. Use one.

Making tables work on mobile

Wide tables are the genuine hard problem in responsive design. Three approaches work, depending on the data.

Horizontal scroll is the simplest and safest: wrap the table in a container with overflow-x: auto, keeping the structure intact. Add tabindex="0" to that wrapper so keyboard users can scroll it too — an easily missed accessibility detail. Stacked cards re-flow each row into a block with labels shown inline, which reads well on phones but needs the labels duplicating in CSS or data attributes. Column hiding shows only essential columns on narrow screens, which works when some columns are genuinely secondary.

Styling that aids comprehension

A few choices measurably improve how quickly people read a table. Zebra striping helps the eye track across long rows. Right-aligning numeric columns lines up the digits so magnitudes are comparable at a glance — a detail that separates a considered table from a default one. Generous cell padding beats tight borders for readability, and a sticky header row keeps context visible while scrolling long data sets.

Rendering API data as a table in React without losing the semantics

The usual shortcut is to take Object.keys(rows[0]) as the headers and the array index as the key. It breaks in three predictable ways. If the first row is missing an optional field, a whole column disappears. Raw keys such as created_at become visible headings. And once the user sorts or filters, index keys make React reuse the wrong row's DOM and state, so a ticked checkbox or an expanded row jumps to a different record. Declare the columns once, with labels and formatting, and key each row by a stable ID from the data.

// Columns are declared, not guessed from Object.keys(rows[0])
const COLUMNS = [
  { key: 'region', label: 'Region', rowHeader: true },
  { key: 'q3', label: 'Q3 revenue', numeric: true },
  { key: 'q4', label: 'Q4 revenue', numeric: true },
];
const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });

export function RevenueTable({ rows, caption }) {
  return (
    <table>
      <caption>{caption}</caption>
      <thead>
        <tr>{COLUMNS.map(c => <th key={c.key} scope="col">{c.label}</th>)}</tr>
      </thead>
      <tbody>
        {rows.map(row => (
          <tr key={row.id}>{/* a stable id, never the array index */}
            {COLUMNS.map(c => c.rowHeader
              ? <th key={c.key} scope="row">{row[c.key]}</th>
              : <td key={c.key} className={c.numeric ? 'num' : undefined}>
                  {c.numeric ? gbp.format(row[c.key]) : row[c.key]}
                </td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

The first cell of each row is a <th scope="row">, so screen readers can announce the region alongside each figure as the user moves along the row. Amounts go through Intl.NumberFormat rather than string concatenation, and the num class should set text-align: right and font-variant-numeric: tabular-nums so the digits line up. If you add sorting, put a <button> inside each sortable <th> and set aria-sort on the column currently sorted. React explains why index keys cause trouble in Rendering Lists, and the W3C's tables tutorial covers headers for more complex layouts.

Frequently asked questions

Is it bad practice to use HTML tables?

Only for page layout. For genuine tabular data — rows and columns of related values — a real table element is the correct, accessible and SEO-friendly choice. Building tables out of divs discards accessibility for no benefit.

What does scope="col" do in a table?

It tells assistive technology that a th cell is the header for its column, so screen readers announce the header alongside each value — "Revenue, Q3: £48,000" instead of an unlabelled number. Use scope="row" for row headers.

How do I make an HTML table responsive?

The simplest reliable method is wrapping it in a container with overflow-x: auto so it scrolls horizontally with its structure intact. Add tabindex="0" to that wrapper so keyboard users can scroll it as well.

Should I add a caption to my table?

Yes where the table needs a title. A caption element gives the table an accessible name that screen readers announce on entry, providing context before the user starts navigating cells.