Skip to content
TablePage.ai Open the app

Table Data in HTML: A Clean, Accessible Pattern

Turn spreadsheet rows into semantic HTML with captions, scoped headers, responsive overflow and safe handling for generated cell values.

Share X in f
Wei Hu

Use an HTML table when values have meaningful row-and-column relationships, such as a publication schedule, price list or research dataset. Do not use one to arrange a page: the HTML standard defines table as multidimensional data and explicitly says tables must not be used as layout aids (WHATWG).

For a simple spreadsheet export, the useful pattern is:

  • caption names the dataset.
  • thead contains the column headers.
  • tbody contains the observations.
  • th identifies row or column headers.
  • td contains ordinary values.

Start with one rectangular dataset

Suppose the source spreadsheet contains this sample data:

station sampled_on nitrate_mg_l status
North Fork 2026-09-12 2.4 Final
Mill Creek 2026-09-12 4.1 Final
Lake Outlet 2026-09-13 3.7 Provisional

These are illustrative values, not real monitoring results. The publication-ready shape has one field per column, one observation per row, a unit in the measurement header and no merged cells.

Keep totals and explanatory notes outside tbody when they are not observations. Otherwise, readers and software may mistake them for ordinary records. See how to separate spreadsheet totals from data rows.

Preserve the distinction between zero, blank and unknown. If the source uses missing-value codes such as NA or -999, document or normalize them before publication rather than displaying them as measurements. The guide to empty CSV fields, zero and missing values covers that decision.

Use this HTML table pattern

The source below escapes its angle brackets as < and > so the publication system displays rather than interprets the markup. Decode those entities when saving the example as HTML.

<p class="dataset-meta">
  Source: Sample monitoring dataset.<br>
  Coverage: 12–13 September 2026.<br>
  Updated: 15 September 2026.<br>
  Unit: milligrams per litre (mg/L).
</p>

<div
  class="table-scroll"
  role="region"
  aria-label="Scrollable table: sample nitrate readings"
  tabindex="0"
>
  <table>
    <caption>Sample nitrate readings, September 2026</caption>
    <thead>
      <tr>
        <th scope="col">Station</th>
        <th scope="col">Sample date</th>
        <th scope="col">Nitrate (mg/L)</th>
        <th scope="col">Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">North Fork</th>
        <td><time datetime="2026-09-12">12 Sep 2026</time></td>
        <td>2.4</td>
        <td>Final</td>
      </tr>
      <tr>
        <th scope="row">Mill Creek</th>
        <td><time datetime="2026-09-12">12 Sep 2026</time></td>
        <td>4.1</td>
        <td>Final</td>
      </tr>
      <tr>
        <th scope="row">Lake Outlet</th>
        <td><time datetime="2026-09-13">13 Sep 2026</time></td>
        <td>3.7</td>
        <td>Provisional</td>
      </tr>
    </tbody>
  </table>
</div>

A tr is a row. A th is a header cell, and a td is a data cell. W3C guidance explains that assistive technologies use the programmatic relationships between headers and data cells to provide context (W3C WAI).

Here, each station name identifies its row, so it is a th with scope="row". If the first value is merely another field—an internal record ID, for example—use td instead. scope="col" associates a heading with its column, while scope="row" associates it with a row (W3C WAI). For grouped or irregular headers, use the patterns in the guide to accessible HTML row and column headers.

The time element preserves a readable display while its datetime attribute provides an unambiguous machine-readable value. Keep one date convention throughout a column and include time-zone context for timestamped events; see the public-table date and time-zone guide.

Name the table and document the data

A caption should identify the table rather than merely say “Data.” W3C describes a caption as functioning like a table heading and notes that most screen readers announce it. A structural summary is usually necessary only for a complex or unusual table (W3C WAI).

Keep provenance and interpretation details near the table. For a real dataset, link to the source and distinguish:

  • the period covered by the data;
  • the source publication date, if known;
  • the date your page was last updated;
  • units and missing-value conventions.

Use the public-table source and date checklist and the detailed guide to captions and summaries.

Let wide tables scroll

Avoid hiding columns just to fit a narrow viewport. Put the table in a horizontal scroll container instead:

.table-scroll {
  max-width: 100%;
  overflow-x: auto;
}

.table-scroll:focus {
  outline: 3px solid #2457a6;
  outline-offset: 2px;
}

table {
  width: 100%;
  min-width: 42rem;
  border-collapse: collapse;
}

caption {
  margin-block-end: 0.5rem;
  font-weight: 700;
  text-align: left;
}

th,
td {
  border: 1px solid #b8b8b8;
  padding: 0.5rem 0.75rem;
  text-align: left;
  vertical-align: top;
}

thead th {
  background: #f2f2f2;
}

Some browsers do not make scrolling areas keyboard-focusable. MDN recommends tabindex="0" for keyboard access, plus an appropriate role and accessible name to give screen-reader users context (MDN). Test the result in the browsers and assistive technologies your site supports.

Insert generated values as text

If JavaScript converts spreadsheet records into table rows, do not pass imported cell values to innerHTML. Create each cell and assign its value with textContent:

function addRow(tbody, record) {
  const tr = document.createElement("tr");

  for (const value of record) {
    const td = document.createElement("td");
    td.textContent = value ?? "";
    tr.append(td);
  }

  tbody.append(tr);
}

MDN advises against using innerHTML to set text because it parses raw markup and can be susceptible to cross-site scripting; textContent inserts text instead (MDN). Server-side templates should use their normal HTML-escaping mechanism for imported values.

For a simple table without spanning cells, validate that every generated row has the same number of cells as the header. A mismatch often points to a malformed import, an unescaped delimiter or a transformation error.

Check the result before publishing

Plain HTML does not add sorting, filtering, pagination or automatic updates. Those behaviors need additional code and accessible controls. If you add sorting, follow the keyboard-accessible sorting pattern.

Before release:

  1. Compare the rendered row count, first record and last record with the source.
  2. Check dates, decimals, units, blanks and known missing values.
  3. Navigate the page and horizontal scroll region with a keyboard.
  4. Verify several body cells against their row and column headers.
  5. Run the page through the W3C Markup Validation Service.

Static HTML suits a small, stable table. For a larger or frequently updated file, compare browser-side JavaScript, static generation and hosted data pages in how to embed CSV data in a website. TablePage currently accepts CSV, TSV, XLSX and XLS files and creates a public dataset page with a filterable table and shareable link (TablePage). Because the resulting page is public, upload only sanitized, non-sensitive data.