Purpose
Represent a single data cell that can contain text, controls, or editable content within a grid structure.
Loading ...
gridcellA cell containing data or interactive controls within a grid or treegrid. Gridcells enable spreadsheet-like navigation and editing while maintaining semantic table relationships.
Grid with editable and read-only cells demonstrating focus management and inline editing.
Cells with ✎ are editable. Double-click or press Enter/F2 to edit. Changes save on blur or Enter.
Represent a single data cell that can contain text, controls, or editable content within a grid structure.
Part of the 2D navigation model—users move with arrow keys across rows and columns.
Announces cell content along with row/column position and any associated headers.
Display records with inline edit buttons, checkboxes, or delete actions in each cell.
Build Excel-like interfaces where users can navigate and edit cell values directly.
Create data displays with clickable cells that reveal details or trigger actions.
Represent days as gridcells that users can select or edit with keyboard navigation.
Every gridcell must be a direct child of an element with role="row". The row must be inside a grid or treegrid.
Either the gridcell itself or an interactive element inside it should receive focus. Use roving tabindex for navigation.
Connect cells to headers using aria-describedby or rely on implicit table semantics when using columnheader/rowheader.
Gridcells can contain text, images, or interactive widgets. Nested grids are not recommended.
| Action | Keys | Result |
|---|---|---|
| Move horizontally | ArrowLeft / ArrowRight | Moves focus to the previous or next gridcell in the same row. |
| Move vertically | ArrowUp / ArrowDown | Moves focus to the gridcell directly above or below in the same column. |
| Jump to row edges | Home / End | Moves focus to the first or last gridcell in the current row. |
| Jump to grid edges | Ctrl + Home / Ctrl + End | Moves focus to the first cell of the grid or the last cell respectively. |
| Enter edit mode | Enter / F2 | Activates editing if the cell is editable, or triggers the primary action. |
| Exit edit mode | Escape | Cancels editing and returns focus to navigation mode. |
| Select cell | Space | Selects the cell or toggles selection when aria-selected is used. |
aria-selectedOptionalIndicates whether the gridcell is selected. Use in grids that support cell selection.
Set to true/false. Omit if selection is not a feature of your grid.
aria-readonlyOptionalIndicates the cell cannot be edited. Defaults to true if not specified on an editable grid.
Override at the cell level when some cells are editable and others are not.
aria-disabledOptionalMarks the cell as non-interactive. The cell is visible but cannot be focused or activated.
aria-colindexOptionalThe 1-based column index of the cell. Required when columns are virtualized or hidden.
Use with aria-colcount on the grid to communicate total columns.
aria-colspanOptionalNumber of columns this cell spans. Only needed when the cell spans multiple columns.
aria-describedbyOptionalReferences the id of the column header to provide context for screen reader users.
aria-expandedOptionalUsed when the gridcell controls expandable content (like a details row or popup).
aria-haspopupOptionalIndicates the cell opens a menu, listbox, or dialog when activated.
Ensure every gridcell is contained within a role="row" element that is inside role="grid" or role="treegrid".
Implement roving tabindex so only one gridcell has tabindex="0" at a time; others should be -1.
Provide visible focus indicators that clearly show which cell is currently focused.
Associate cells with their headers using aria-describedby or proper columnheader/rowheader placement.
Handle Enter/F2 for edit mode entry and Escape for canceling edits on editable cells.
Update aria-selected immediately when users select or deselect cells.
For virtualized grids, maintain accurate aria-colindex values as the viewport changes.
Ensure interactive content within cells (buttons, links) is keyboard accessible.
Simple grid structure showing proper role hierarchy and navigation setup.
<div role="grid" aria-label="Employee Directory" aria-rowcount="3" aria-colcount="4">
<!-- Header Row -->
<div role="row">
<div role="columnheader" id="col-name">Name</div>
<div role="columnheader" id="col-dept">Department</div>
<div role="columnheader" id="col-status">Status</div>
<div role="columnheader" id="col-actions">Actions</div>
</div>
<!-- Data Rows -->
<div role="row">
<div role="gridcell" tabindex="0" aria-describedby="col-name">Sarah Chen</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-dept">Engineering</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-status">Active</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-actions">
<button tabindex="-1">Edit</button>
</div>
</div>
<div role="row">
<div role="gridcell" tabindex="-1" aria-describedby="col-name">Marcus Johnson</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-dept">Design</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-status">Away</div>
<div role="gridcell" tabindex="-1" aria-describedby="col-actions">
<button tabindex="-1">Edit</button>
</div>
</div>
</div>Grid with cell selection using aria-selected for multi-select functionality.
<div role="grid" aria-label="Data Selection" aria-multiselectable="true">
<div role="row">
<div role="columnheader">Product</div>
<div role="columnheader">Price</div>
<div role="columnheader">Stock</div>
</div>
<div role="row">
<div role="gridcell" aria-selected="false" tabindex="0">Widget A</div>
<div role="gridcell" aria-selected="true" tabindex="-1">$29.99</div>
<div role="gridcell" aria-selected="false" tabindex="-1">142</div>
</div>
<div role="row">
<div role="gridcell" aria-selected="false" tabindex="-1">Widget B</div>
<div role="gridcell" aria-selected="false" tabindex="-1">$49.99</div>
<div role="gridcell" aria-selected="true" tabindex="-1">87</div>
</div>
</div>
<script>
// Selection handling
grid.addEventListener('keydown', (e) => {
if (e.key === ' ' && e.target.role === 'gridcell') {
e.preventDefault();
const isSelected = e.target.getAttribute('aria-selected') === 'true';
e.target.setAttribute('aria-selected', String(!isSelected));
}
});
</script>Implementation of edit mode with Enter to edit and Escape to cancel.
function EditableGridCell({ value, onChange, colHeader }) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const cellRef = useRef(null);
const inputRef = useRef(null);
const handleKeyDown = (e) => {
if (!isEditing) {
if (e.key === 'Enter' || e.key === 'F2') {
e.preventDefault();
setIsEditing(true);
setEditValue(value);
}
} else {
if (e.key === 'Enter') {
e.preventDefault();
onChange(editValue);
setIsEditing(false);
cellRef.current?.focus();
} else if (e.key === 'Escape') {
e.preventDefault();
setEditValue(value);
setIsEditing(false);
cellRef.current?.focus();
}
}
};
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
return (
<div
ref={cellRef}
role="gridcell"
tabIndex={-1}
aria-readonly={!isEditing}
aria-describedby={colHeader}
onKeyDown={handleKeyDown}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={() => {
onChange(editValue);
setIsEditing(false);
}}
aria-label="Edit cell value"
/>
) : (
<span>{value}</span>
)}
</div>
);
}JavaScript implementation of 2D grid navigation with roving tabindex.
class GridNavigation {
constructor(gridElement) {
this.grid = gridElement;
this.rows = Array.from(gridElement.querySelectorAll('[role="row"]'));
this.activeRow = 0;
this.activeCol = 0;
this.initializeFocus();
this.bindEvents();
}
getCells(rowIndex) {
return Array.from(this.rows[rowIndex].querySelectorAll('[role="gridcell"]'));
}
initializeFocus() {
// Set all cells to tabindex="-1" except the first
this.rows.forEach((row, rowIdx) => {
this.getCells(rowIdx).forEach((cell, colIdx) => {
cell.tabIndex = (rowIdx === 0 && colIdx === 0) ? 0 : -1;
});
});
}
setActiveCell(rowIdx, colIdx) {
// Remove focus from current cell
const currentCell = this.getCells(this.activeRow)[this.activeCol];
if (currentCell) currentCell.tabIndex = -1;
// Bounds checking
const maxRow = this.rows.length - 1;
const cells = this.getCells(rowIdx);
const maxCol = cells.length - 1;
this.activeRow = Math.max(0, Math.min(rowIdx, maxRow));
this.activeCol = Math.max(0, Math.min(colIdx, maxCol));
// Set focus on new cell
const newCell = this.getCells(this.activeRow)[this.activeCol];
if (newCell) {
newCell.tabIndex = 0;
newCell.focus();
}
}
bindEvents() {
this.grid.addEventListener('keydown', (e) => {
const handlers = {
'ArrowRight': () => this.setActiveCell(this.activeRow, this.activeCol + 1),
'ArrowLeft': () => this.setActiveCell(this.activeRow, this.activeCol - 1),
'ArrowDown': () => this.setActiveCell(this.activeRow + 1, this.activeCol),
'ArrowUp': () => this.setActiveCell(this.activeRow - 1, this.activeCol),
'Home': () => this.setActiveCell(this.activeRow, 0),
'End': () => this.setActiveCell(this.activeRow, Infinity),
};
if (handlers[e.key]) {
e.preventDefault();
handlers[e.key]();
}
});
}
}
// Usage
const grid = document.querySelector('[role="grid"]');
new GridNavigation(grid);Implement roving tabindex so users can Tab into the grid and use arrows to navigate. Only one cell should be tabbable.
Connect cells to column and row headers so screen readers announce context (e.g., "Price: $29.99").
Visually distinguish between navigation mode and edit mode. Show clear indicators for editable cells.
Ensure aria-colindex and aria-rowindex are accurate, especially in virtualized grids.
Click interactions should update the active cell and focus state to match keyboard behavior.
Provide high-contrast focus rings that are visible against cell backgrounds in both light and dark modes.
Placing gridcells directly inside the grid element without wrapping them in rows.
Always nest gridcells inside role="row" elements. The row provides context for screen readers.
<div role="grid">
<div role="gridcell">Cell 1</div>
<div role="gridcell">Cell 2</div>
</div><div role="grid">
<div role="row">
<div role="gridcell">Cell 1</div>
<div role="gridcell">Cell 2</div>
</div>
</div>Setting tabindex="0" on every gridcell forces users to tab through hundreds of cells.
Use roving tabindex: only the active cell has tabindex="0", all others have tabindex="-1".
<div role="row">
<div role="gridcell" tabindex="0">A</div>
<div role="gridcell" tabindex="0">B</div>
<div role="gridcell" tabindex="0">C</div>
</div><div role="row">
<div role="gridcell" tabindex="0">A</div>
<div role="gridcell" tabindex="-1">B</div>
<div role="gridcell" tabindex="-1">C</div>
</div>Arrow keys do not move between cells, breaking the expected grid interaction model.
Implement arrow key handlers that move focus between cells in 2D space.
Buttons and links inside cells are in the tab order, disrupting grid navigation.
Set tabindex="-1" on interactive content inside cells. Users access them via Enter on the cell.
<div role="gridcell" tabindex="-1">
<button>Edit</button>
<button>Delete</button>
</div><div role="gridcell" tabindex="-1">
<button tabindex="-1">Edit</button>
<button tabindex="-1">Delete</button>
</div>Screen readers announce cell values without column context, confusing users.
Use aria-describedby to link cells to their column headers, or use proper HTML table structure.
Parent container that provides the grid context for navigation.
Learn moreRequired parent element that groups gridcells horizontally.
Learn moreHeader cell that labels a column of gridcells.
Learn moreHeader cell that labels a row of gridcells.
Learn moreNon-interactive table cell. Use gridcell when cells are interactive.
Learn moreHierarchical grid where gridcells appear in expandable rows.
Learn more