aria-readonly and aria-disabled in Grid Cells
Permalink to "aria-readonly and aria-disabled in Grid Cells"In an editable grid, some cells cannot be changed. A computed total is read-only: its value is real and meant to be read and copied, but not typed over. A field locked while a record is being approved is disabled: temporarily unavailable, for a reason that may change. ARIA has an attribute for each — aria-readonly and aria-disabled — and they are announced differently. Using the wrong one, or neither, leaves users pressing Enter on cells that silently refuse to edit.
This page covers where each attribute is valid, how screen readers announce it, and why both kinds of cell should stay focusable. It belongs to composite widget roles & states.
Spec reference
Permalink to "Spec reference"aria-readonly (true | false) indicates that an element is not editable but is otherwise operable. It is supported on gridcell, columnheader, rowheader, grid, treegrid, textbox, combobox, checkbox and a few others. On a grid, it sets the default for all cells; an individual cell can override with aria-readonly="false".
aria-disabled (true | false) indicates that an element is perceivable but disabled — not editable or otherwise operable. It is a global attribute in ARIA 1.2 terms for widgets, and on a container it applies to all descendants.
Unlike the HTML disabled attribute, neither ARIA attribute removes the element from the tab order or blocks events. Your code must make edit keys do nothing on these cells.
Criteria: SC 4.1.2 Name, Role, Value — the state must be exposed; SC 3.3.2 Labels or Instructions — users need to know why an expected action is unavailable when it is not self-evident.
When to use each — and when neither
Permalink to "When to use each — and when neither"Read-only for values that are never editable in this grid: identifiers, computed totals, timestamps, data owned by another system. Set it on the cell, or set aria-readonly="true" on the grid and false on the editable cells if most cells are read-only.
Disabled for values that are editable in principle but not right now: a record locked by another user, a period that has been closed, a field that depends on another field being set first. Pair it with a description of why.
Neither in a static table. A native <table> has no editing model, so there is nothing to be read-only relative to. The attributes only mean something in a grid where other cells are editable.
The misapplication to name is removing locked cells from the focus order (tabindex omitted, or skipped by the arrow handler). Users then cannot read the locked values at all, and arrowing across a row jumps unpredictably. Keep every cell focusable; block editing, not navigation.
Annotated code example
Permalink to "Annotated code example"<div role="grid" aria-label="Q1 budget" aria-readonly="false">
<div role="row">
<div role="rowheader">Travel</div>
<!-- editable -->
<div role="gridcell" tabindex="0">4,000</div>
<!-- SC 4.1.2: computed; read and copy, never edit -->
<div role="gridcell" tabindex="-1" aria-readonly="true">12,400</div>
<!-- SC 4.1.2 + 3.3.2: temporarily locked, with the reason -->
<div role="gridcell" tabindex="-1" aria-disabled="true"
aria-describedby="lock-q1">3,900</div>
</div>
</div>
<p id="lock-q1" class="visually-hidden">Locked: Q1 is closed for approval.</p>
// Edit keys respect the state; navigation does not
grid.addEventListener('keydown', (e) => {
const cell = e.target.closest('[role="gridcell"]');
if (!cell) return;
if (e.key === 'Enter' || e.key === 'F2' || isPrintable(e)) {
if (cell.getAttribute('aria-readonly') === 'true') {
status('Read only.'); // optional, brief
e.preventDefault(); return;
}
if (cell.getAttribute('aria-disabled') === 'true') {
const why = document.getElementById(cell.getAttribute('aria-describedby'))?.textContent;
status(why || 'Unavailable.'); // say why
e.preventDefault(); return;
}
startEdit(cell);
}
// Arrow keys fall through to normal navigation for every cell
});
/* Visual states that do not rely on colour alone (SC 1.4.1) */
[role="gridcell"][aria-readonly="true"] { background: var(--cell-readonly-bg); }
[role="gridcell"][aria-readonly="true"]::after { content: "🔒︎" / ""; margin-inline-start: .25rem; }
[role="gridcell"][aria-disabled="true"] { color: var(--text-muted); text-decoration: line-through dotted; }
The decorative lock uses empty alternative text for the generated content so it is not read on top of the “read only” state the attribute already announces.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | NVDA + Chrome | JAWS + Chrome | VoiceOver + Safari |
|---|---|---|---|
| Arrow to read-only cell | “12,400, read only” | “12,400, read only” | “12,400, read-only” |
| Arrow to disabled cell | “3,900, unavailable, Locked: Q1 is closed…” | “3,900, unavailable” then description | “3,900, dimmed” |
Enter on read-only cell |
“Read only.” (status) | Same | Same |
Ctrl+C on read-only cell |
Copies the value | Same | Same |
Grid-level aria-readonly="true" |
“read only” announced on entering grid | On each cell | On entering grid |
Integration context
Permalink to "Integration context"These states interact with paste: a pasted block that covers read-only or disabled cells should skip them and report them as rejects, as described in keyboard copy and paste in data grids. And they gate the edit-mode keys defined in entering and exiting cell edit mode.
Explaining a locked column once, in its header, is often better than a description on every cell; see aria-describedby for column help text.
Gotchas
Permalink to "Gotchas"HTML disabled on inputs inside cells. A disabled <input> inside a cell is removed from focus and some readers skip it. Render the value as text with aria-disabled on the cell instead.
aria-disabled on a container. Setting it on a row disables every cell in it — correct for a locked record, surprising if you meant one field.
Colour-only states. A grey background for read-only and a lighter grey for disabled is unreadable for many users. Add a non-colour cue as in the CSS example.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"What is the difference between aria-readonly and aria-disabled?
aria-readonly means the value can be read, selected and copied but not changed. aria-disabled means the element is unavailable for interaction at the moment, usually for a reason that can change. Screen readers announce them as “read only” and “unavailable” respectively.
Should disabled grid cells be focusable?
Yes. Keep every cell reachable so users can read locked values and learn why they are locked. Block editing in your key handler instead of removing the cell from navigation.
Can aria-readonly be set on the whole grid?
Yes. It becomes the default for all cells, and individual cells can set aria-readonly=“false” to be editable. That is convenient when most of a grid is read-only.
Do these attributes stop keyboard events?
No. Unlike HTML disabled, ARIA states only change what is exposed to assistive technology. Your code must ignore edit keys on read-only and disabled cells.
Related
Permalink to "Related"- Entering and exiting edit mode — the keys these states block
- Keyboard copy and paste — paste rejects on read-only cells
- aria-describedby for column help — explaining why a cell is locked