Purpose
Present tabular data with keyboard navigation, sorting, and optional inline editing.
Loading ...
gridAn interactive data surface that arranges information in rows and columns. People can navigate, edit, or select cells similar to a spreadsheet or data table.
Spreadsheet-like grid with cell-level focus management and arrow key navigation.
Present tabular data with keyboard navigation, sorting, and optional inline editing.
Contains row elements composed of gridcell, columnheader, or rowheader roles.
Announces row/column coordinates and supports table-style reading commands.
Financial dashboards, issue trackers, or CMS listing screens that require quick editing.
Provide arrow-key navigation and editing in place without leaving the context of the table.
Represent days, times, or seats in grid formations with consistent keyboard controls.
Each direct child of the grid must be role="row". Each row must contain role="gridcell" (or header) children.
Only one cell should be in the tab order at a time. Implement roving tabindex or aria-activedescendant.
Include columnheader / rowheader cells and connect them via aria-describedby or aria-labelledby for clarity.
| Action | Keys | Result |
|---|---|---|
| Move within row | ArrowLeft / ArrowRight | Focuses the adjacent gridcell in the same row. |
| Move between rows | ArrowUp / ArrowDown | Focuses the gridcell above or below while staying in the same column. |
| Jump to edges | Home / End | Moves to the first or last cell in the current row. |
| Jump to top/bottom | Ctrl + Home / Ctrl + End | Moves to the first cell of the grid or the last cell respectively. |
| Page navigation | PageUp / PageDown | Scrolls the view by a page and updates focus to the same column in the newly visible row. |
| Selection toggle | Space / Shift + Arrow | Marks the current cell (or range) as selected when aria-multiselectable is true. |
aria-rowcountOptionalTotal number of rows. Use when virtualizing so assistive tech knows the dataset size.
aria-colcountOptionalTotal number of columns, especially when columns can be hidden or virtualized.
aria-rowindexOptionalApplied to rows to represent their 1-based index inside the full data set.
aria-colindexOptionalApplied to gridcells to communicate the column index.
aria-multiselectableOptionalSet to true when multiple cells, rows, or columns can be selected simultaneously.
aria-activedescendantOptionalAlternative to roving tabindex; reference the focused cell id while keeping DOM focus on the grid.
aria-readonlyOptionalMark the entire grid or specific cells as read-only to manage editing expectations.
Preserve DOM order that matches the visual grid so screen readers can follow relationships.
Provide clear cell boundaries and focus outlines so sighted keyboard users can track position.
Announce sorting state with aria-sort on columnheader elements whenever the user sorts a column.
If editing is supported, describe the mode switch (e.g., Enter to edit, Escape to cancel).
For large datasets, virtualize rows but keep aria-rowcount/aria-rowindex accurate to avoid confusion.
Shows explicit row and gridcell roles with headers bound via aria-describedby.
<div role="grid" aria-rowcount="3" aria-colcount="3">
<div role="row">
<div role="columnheader" id="col-name">Name</div>
<div role="columnheader" id="col-role">Role</div>
<div role="columnheader" id="col-status">Status</div>
</div>
<div role="row">
<div role="rowheader" id="row-1">Order #2458</div>
<div role="gridcell" aria-describedby="col-role row-1">Fulfillment</div>
<div role="gridcell" aria-describedby="col-status row-1">In progress</div>
</div>
</div>JavaScript roves tabindex so only one cell is in the tab order.
const grid = document.querySelector('[role="grid"]')
const cells = Array.from(grid.querySelectorAll('[role="gridcell"]'))
function setActiveCell(nextIndex) {
cells.forEach((cell, index) => {
cell.tabIndex = index === nextIndex ? 0 : -1
})
cells[nextIndex].focus()
}
let activeIndex = 0
setActiveCell(activeIndex)
grid.addEventListener('keydown', (event) => {
const colCount = 3
if (event.key === 'ArrowRight') {
event.preventDefault()
activeIndex = Math.min(activeIndex + 1, cells.length - 1)
} else if (event.key === 'ArrowLeft') {
event.preventDefault()
activeIndex = Math.max(activeIndex - 1, 0)
} else if (event.key === 'ArrowDown') {
event.preventDefault()
activeIndex = Math.min(activeIndex + colCount, cells.length - 1)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
activeIndex = Math.max(activeIndex - colCount, 0)
} else {
return
}
setActiveCell(activeIndex)
})Repeatedly announce column headers with aria-describedby so users never lose context.
Visually show row/column indices to mirror what assistive tech announces.
When data updates, keep the active cell in place to avoid focus loss.
A div with role="grid" contains plain div children. Screen readers cannot understand the structure.
Apply role="row" to each row container and role="gridcell"/header to each cell.
Every gridcell has tabindex="0", forcing users to tab hundreds of times.
Implement roving tabindex or aria-activedescendant so only one cell is tabbable.
Child container that groups gridcells horizontally.
Learn moreInteractive cell that contains data or controls.
Learn moreHybrid between grid and tree to express hierarchy.
Learn more