Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget RolesInteractive cellGrid childSupports editing

gridcell

A cell containing data or interactive controls within a grid or treegrid. Gridcells enable spreadsheet-like navigation and editing while maintaining semantic table relationships.

Live Example

Interactive Gridcell Example

Grid with editable and read-only cells demonstrating focus management and inline editing.

↑↓←→ NavigateEnter/F2 EditEsc Cancel
Cell [1, 1] • Editable
Name
Role
Email
Status
Sarah Chen
Engineer
sarah@example.com
Active
Marcus Johnson
Designer
marcus@example.com
Away
Elena Rodriguez
PM
elena@example.com
Active

Cells with ✎ are editable. Double-click or press Enter/F2 to edit. Changes save on blur or Enter.

Purpose

Represent a single data cell that can contain text, controls, or editable content within a grid structure.

Navigation

Part of the 2D navigation model—users move with arrow keys across rows and columns.

Screen Reader

Announces cell content along with row/column position and any associated headers.

When to Use

1

Data Tables with Actions

Display records with inline edit buttons, checkboxes, or delete actions in each cell.

2

Spreadsheet Applications

Build Excel-like interfaces where users can navigate and edit cell values directly.

3

Interactive Dashboards

Create data displays with clickable cells that reveal details or trigger actions.

4

Calendar Grids

Represent days as gridcells that users can select or edit with keyboard navigation.

Required Structure

Parent Row Required

Every gridcell must be a direct child of an element with role="row". The row must be inside a grid or treegrid.

Focusable Model

Either the gridcell itself or an interactive element inside it should receive focus. Use roving tabindex for navigation.

Header Association

Connect cells to headers using aria-describedby or rely on implicit table semantics when using columnheader/rowheader.

Content Restrictions

Gridcells can contain text, images, or interactive widgets. Nested grids are not recommended.

Keyboard Interaction Model

ActionKeysResult
Move horizontallyArrowLeft / ArrowRightMoves focus to the previous or next gridcell in the same row.
Move verticallyArrowUp / ArrowDownMoves focus to the gridcell directly above or below in the same column.
Jump to row edgesHome / EndMoves focus to the first or last gridcell in the current row.
Jump to grid edgesCtrl + Home / Ctrl + EndMoves focus to the first cell of the grid or the last cell respectively.
Enter edit modeEnter / F2Activates editing if the cell is editable, or triggers the primary action.
Exit edit modeEscapeCancels editing and returns focus to navigation mode.
Select cellSpaceSelects the cell or toggles selection when aria-selected is used.

Required States & Properties

aria-selectedOptional

Indicates 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-readonlyOptional

Indicates 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-disabledOptional

Marks the cell as non-interactive. The cell is visible but cannot be focused or activated.

aria-colindexOptional

The 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-colspanOptional

Number of columns this cell spans. Only needed when the cell spans multiple columns.

aria-describedbyOptional

References the id of the column header to provide context for screen reader users.

aria-expandedOptional

Used when the gridcell controls expandable content (like a details row or popup).

aria-haspopupOptional

Indicates the cell opens a menu, listbox, or dialog when activated.

Implementation Checklist

  • 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.

Code Examples

Basic Grid with Gridcells

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>

Selectable Gridcells

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>

Editable Gridcell Pattern

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>
  );
}

Roving Tabindex Implementation

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);

Best Practices

🎯

Focus Management

Implement roving tabindex so users can Tab into the grid and use arrows to navigate. Only one cell should be tabbable.

🏷️

Header Associations

Connect cells to column and row headers so screen readers announce context (e.g., "Price: $29.99").

✏️

Clear Edit States

Visually distinguish between navigation mode and edit mode. Show clear indicators for editable cells.

📍

Position Awareness

Ensure aria-colindex and aria-rowindex are accurate, especially in virtualized grids.

🖱️

Pointer Parity

Click interactions should update the active cell and focus state to match keyboard behavior.

👁️

Visible Focus

Provide high-contrast focus rings that are visible against cell backgrounds in both light and dark modes.

Common Mistakes

Gridcell Outside Row

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.

Incorrect
<div role="grid">
  <div role="gridcell">Cell 1</div>
  <div role="gridcell">Cell 2</div>
</div>
Correct
<div role="grid">
  <div role="row">
    <div role="gridcell">Cell 1</div>
    <div role="gridcell">Cell 2</div>
  </div>
</div>

All Cells Tabbable

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".

Incorrect
<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>
Correct
<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>

Missing Keyboard Navigation

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.

Interactive Elements Steal Focus

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.

Incorrect
<div role="gridcell" tabindex="-1">
  <button>Edit</button>
  <button>Delete</button>
</div>
Correct
<div role="gridcell" tabindex="-1">
  <button tabindex="-1">Edit</button>
  <button tabindex="-1">Delete</button>
</div>

No Header Context

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.

Related Roles & Patterns

grid

Parent container that provides the grid context for navigation.

Learn more

row

Required parent element that groups gridcells horizontally.

Learn more

columnheader

Header cell that labels a column of gridcells.

Learn more

rowheader

Header cell that labels a row of gridcells.

Learn more

cell

Non-interactive table cell. Use gridcell when cells are interactive.

Learn more

treegrid

Hierarchical grid where gridcells appear in expandable rows.

Learn more