Max Design

Published:

When people ask about an “editable table”, they’re often describing very different interaction models. Before choosing a solution, it helps to understand the three most common approaches and what each one is actually good at.


1. Native HTML table with form controls

This approach uses a standard HTML <table> and places native form controls inside table cells.

The table remains a document-style structure, and users move between inputs using TAB and SHIFT + TAB.

The browser and assistive technologies handle almost all navigation and semantics automatically.

Code example

<table>
  <caption>Order quantities</caption>
  <thead>
    <tr>
      <th>ID number</th>
      <th>Product</th>
      <th id="col-qty">Quantity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>A234</td>
      <th id="prod-a234">6mm Stainless Coach Screws</th>
      <td>
        <input
          type="number"
          inputmode="numeric"
          min="0"
          placeholder="0"
          aria-labelledby="col-qty prod-a234"
        />
      </td>
    </tr>
  </tbody>
</table>

In this example, the <input> does not have a visible label. Instead, it is labelled using the table’s column header (“Quantity”) and row header (the product name). The placeholder is used only as a visual hint, not as a label.

When this is the best option

This should be the default choice unless you have a clear reason not to use it.


2. ARIA table with custom row wrappers

This approach uses non-table elements styled as a table, with ARIA table roles applied.

Because the markup is no longer constrained by the table content model, you can wrap rows with additional elements for layout, styling, or interaction.

The structure still exposes rows and cells to assistive technologies, but you take on more responsibility for getting it right.

Code example

<div role="table" aria-label="Order quantities">
  <div role="rowgroup">
    <div role="row">
      <div role="columnheader">ID number</div>
      <div role="columnheader">Product</div>
      <div role="columnheader" id="col-qty">Quantity</div>
      <div role="columnheader">Actions</div>
    </div>
  </div>

  <div role="rowgroup">
    <!-- Wrapper used for styling, JS hooks -->
    <div class="row-wrapper" data-item-id="A234">
      <!-- Optional extra affordance outside the cells -->
      <button
        type="button"
        class="drag-handle"
        aria-label="Reorder item A234"
      >
        ⠿
      </button>

      <div role="row">
        <div role="cell">A234</div>

        <div role="cell" id="prod-a234">
          6mm Stainless Coach Screws
        </div>

        <div role="cell">
          <input
            type="number"
            inputmode="numeric"
            min="0"
            placeholder="0"
            aria-labelledby="col-qty prod-a234"
          />
        </div>

        <div role="cell">
          <button
            type="button"
            class="delete-row"
            aria-label="Delete 6mm Stainless Coach Screws"
          >
            Delete
          </button>
        </div>
      </div>
    </div>
  </div>
</div>

This approach trades native table guarantees for layout and interaction flexibility. See caveats below.

When this is the best option


3. ARIA grid (application-style table)

An ARIA grid represents an application-style data widget, not a document-style table. It is designed for experiences where users must move between cells using the keyboard (for example with arrow keys), similar to a spreadsheet.

In an ARIA grid, data cells use role="gridcell" instead of role="cell", which signals that the table behaves as an interactive widget rather than a document.

The following example shows HTML structure only and does not include the JavaScript required to make the grid usable.

Code example

<div
  role="grid"
  aria-label="Simple editable grid"
  tabindex="0"
  aria-rowcount="2"
  aria-colcount="2"
>
  <div role="row">
    <div role="columnheader">Name</div>
    <div role="columnheader">Email</div>
  </div>
  <div role="row">
    <div role="gridcell">Asha</div>
    <div role="gridcell">
      <label for="grid-email-asha">Email</label>
      <input
        id="grid-email-asha"
        name="grid-email-asha"
        type="email"
        value="asha@example.com"
      />
    </div>
  </div>
</div>

This pattern is intended for spreadsheet-style, keyboard-driven interaction. See caveats below.

When this is the best option


Final takeaway

Use a native table where possible. They are accessible, stable and semantic.

Use an ARIA table only when you need additional markup around rows or cells.

Use a grid only when you are genuinely building a data-driven application that requires keyboard navigation within the table.


Caveats and implementation notes

The following notes expand on the trade-offs and risks of the more complex patterns described above.

Example 2: ARIA tables with custom row wrappers

This example shows a hypothetical ARIA table used to illustrate why teams sometimes move away from native tables, and what that extra flexibility enables.

The key difference from a native table is the presence of an element wrapping each row. This wrapper exists purely to provide layout and interaction flexibility that HTML tables do not allow. For example, it can be used to:

Importantly, the wrapper itself is not interactive by default. Instead, this pattern supports two common approaches to row-level interaction:

Explicit actions inside the row, such as a “Delete” button placed in a dedicated Actions cell. This is the most robust and accessible option.

Row-level interaction managed via JavaScript, where the wrapper acts as a hook for selection, drag-and-drop, or other behaviours that do not map cleanly to a native table.

This example is intentionally simplified. It does not prescribe a specific interaction model, but demonstrates how ARIA table roles can be used to preserve table semantics while allowing patterns that are not possible with native <table> markup.

Example 3: aria-grid

Adding role="grid" changes how assistive technologies interpret the region and creates expectations of spreadsheet-style interaction. Without additional behaviour, an ARIA grid is incomplete and should not be considered usable.

A usable grid typically requires JavaScript to handle:

This pattern has the highest implementation and testing cost of all three options. It should only be used when keyboard-driven, cell-by-cell interaction is a core requirement.

Any use of an ARIA grid should be tested across multiple browsers and screen readers.


Further reading