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
- Editing happens via standard form fields
- Users move between fields using TAB
- The table is primarily a document, not an application
- You want the lowest accessibility risk and testing cost
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
- You need wrappers around rows for layout or interaction
- Row-level UI cannot fit cleanly inside a native table
- The interaction model is still table-like, not spreadsheet-like
- You are comfortable validating structure and announcements across screen readers
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
- Users must move cell-to-cell using arrow keys
- Spreadsheet-style interaction is a core requirement
- You need selection, ranges, or dense data editing
- You have time and expertise to implement and test it properly
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:
- attach JavaScript hooks,
- add hover or selection styling,
- provide drag handles or other non-tabular affordances.
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:
- arrow key navigation between cells
- focus management (roving tabindex or active descendant)
- navigation vs editing modes
- announcements of the current row, column, and cell
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
- ARIA Authoring Practices: Table pattern
- ARIA Authoring Practices: Grid pattern
- Hey, It’s Still OK to Use Tables — Adrian Roselli
- Be Careful Using ‘Grid’ — Adrian Roselli