Entering and Exiting Cell Edit Mode With Enter, F2 and Escape
Permalink to "Entering and Exiting Cell Edit Mode With Enter, F2 and Escape"An editable data grid has two keyboard modes. In navigation mode, arrow keys move focus from cell to cell. In edit mode, focus is inside an input and arrow keys move the text caret. The mode switch is the single most important part of an editable grid’s keyboard contract, and getting it wrong traps users: either they can never type into a cell, or once they do they can never arrow out of it.
This page defines the switch — which keys enter, which commit, which cancel — and how the change is communicated. It belongs to inline editing & form controls and assumes the grid already has roving tabindex for navigation mode.
Spec reference
Permalink to "Spec reference"The ARIA Authoring Practices grid pattern describes this two-mode model. In navigation mode the focused element is the gridcell (or a single widget inside it). Pressing Enter or F2 “disables grid navigation” and moves focus into the cell’s content; Escape restores grid navigation. The pattern also allows typing a printable character to start editing with that character replacing the value, as spreadsheets do.
aria-readonly="true" on a cell or the grid indicates cells that cannot enter edit mode, which users otherwise discover only by pressing Enter and getting nothing.
Criteria in play: SC 2.1.1 Keyboard and SC 2.1.2 No Keyboard Trap (both Level A) — the user must be able to enter and leave every mode by keyboard. SC 3.2.2 On Input — committing an edit must not trigger an unexpected context change, such as re-sorting the row away from under focus.
When to use this model — and when not to
Permalink to "When to use this model — and when not to"Use the two-mode model for grids where cells are edited occasionally and navigated constantly: financial ledgers, inventory sheets, admin tables. Navigation stays fast, and editing is explicit.
For a form that happens to be laid out in rows — each row a few inputs a user fills in sequence — skip the grid entirely. A table of native inputs with normal Tab order is simpler and needs no mode at all.
The misapplication to name is making every cell permanently an <input> inside role="grid". Arrow keys then move the caret, not the cell, and there is no way to navigate the grid except Tab, which visits hundreds of inputs. That is technically keyboard operable and practically unusable.
Annotated code example
Permalink to "Annotated code example"// Navigation-mode handler on the grid; edit-mode handler on the input
grid.addEventListener('keydown', (e) => {
const cell = e.target.closest('[role="gridcell"]');
if (!cell || cell.dataset.editing === 'true') return; // edit mode owns keys
if ((e.key === 'Enter' || e.key === 'F2') && isEditable(cell)) {
e.preventDefault();
startEdit(cell, { keep: true }); // SC 2.1.1
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && isEditable(cell)) {
startEdit(cell, { keep: false, initial: e.key }); // spreadsheet-style
e.preventDefault();
} else {
moveFocusByArrow(e, cell); // navigation mode
}
});
function startEdit(cell, { keep, initial = '' }) {
const original = cell.dataset.value;
const input = document.createElement('input');
input.value = keep ? original : initial;
// SC 4.1.2: the input is named by its column header
input.setAttribute('aria-label', headerFor(cell));
cell.dataset.editing = 'true';
cell.replaceChildren(input);
input.focus();
if (keep) input.select();
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); finish(cell, input.value); }
if (e.key === 'Escape') { e.preventDefault(); finish(cell, original); } // SC 2.1.2
if (e.key === 'Tab') { finish(cell, input.value, { thenMove: e.shiftKey ? -1 : 1 }); e.preventDefault(); }
});
}
function finish(cell, value, { thenMove } = {}) {
cell.dataset.value = value;
cell.dataset.editing = 'false';
cell.textContent = value;
// SC 2.4.3 + 3.2.2: focus returns to the SAME cell; no re-sort under the user
const target = thenMove ? nextEditable(cell, thenMove) : cell;
target.focus();
}
The mode is conveyed by focus, not by an announcement. When focus moves into an input, every screen reader announces the input — “Amount, edit text, 1,280.00, selected” — which is itself the signal that edit mode has started. When focus returns to the cell, the cell is read again with its new value. Adding “edit mode” through a live region duplicates this and competes with it.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Mode | Action | Expected announcement |
|---|---|---|---|
Enter / F2 |
Navigation | Enter edit mode, keep value | “Amount, edit text, 1,280.00, selected” |
| Printable key | Navigation | Enter edit mode, replace value | “Amount, edit text, 9” |
| Arrow keys | Edit | Move caret within the text | Characters under the caret |
Enter |
Edit | Commit, return to cell | “1,300.00, Amount column” |
Escape |
Edit | Cancel, restore value | “1,280.00, Amount column” |
Tab |
Edit | Commit, move to next editable cell | Next cell’s editor or value |
Integration context
Permalink to "Integration context"Commit is where validation happens. If the value is invalid, the editor stays open, focus stays in the input, and the error is attached with aria-describedby — see inline form validation inside editable table cells. A committed-but-regretted edit is handled by the undo patterns in undo and cancel affordances for inline cell edits.
Editors that need arrow keys for themselves — a <select>, a date picker — complicate the edit-mode rules further; that case is covered in select and date editors inside grid cells.
Gotchas
Permalink to "Gotchas"Re-sorting on commit. If the grid is sorted by the edited column, the committed row may move. Keep it in place until the next explicit sort, or move focus with it and announce the new position — never leave focus on whatever row slid into its place.
Escape bubbling to a dialog. A grid inside a modal will close the modal on Escape unless the editor stops propagation. Cancel the edit first; a second Escape in navigation mode can close the dialog.
Printable-key entry and screen readers. In browse mode, NVDA and JAWS consume letter keys as quick-navigation commands before your handler sees them. This is expected: users switch to focus mode (or the grid role triggers it automatically). Do not try to work around it.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"How do users know a cell is read-only before pressing Enter?
Mark it with aria-readonly=“true” on the cell, or on the whole grid with exceptions marked false. Screen readers announce “read only” as the user arrives on the cell, so pressing Enter and getting nothing is no longer a surprise.
Which keys should start editing a grid cell?
Enter and F2 should start editing while keeping the current value; typing a printable character can start editing with that character replacing the value. Document the behaviour and apply it consistently across all editable columns.
Should the grid announce "edit mode" when editing starts?
Not through a live region. Moving focus into the input already makes every screen reader announce the input’s name, role and value, which is the signal. An extra announcement competes with it.
What should Tab do while a cell is being edited?
The most common contract is to commit the edit and move to the next editable cell, with Shift+Tab going backwards, and to leave the grid from the last editable cell. Whatever you choose, it must never trap the user, and it should be written into the grid’s keyboard help.
Related
Permalink to "Related"- Inline validation inside editable cells — what happens when a commit fails
- Undo and cancel affordances — recovering after a commit
- Select and date editors in cells — editors that use arrow keys themselves