Naming Icon-Only Row Action Buttons
Permalink to "Naming Icon-Only Row Action Buttons"Every row in a typical admin table ends with the same small icons: a pencil, a copy symbol, a bin, a three-dot menu. Visually, their meaning comes from the icon and their target comes from the row they sit in. A screen reader user gets neither by default: an SVG icon inside a <button> gives it no name at all, and a generic aria-label="Delete" gives a list of forty identical “Delete, button” entries with nothing to say which one deletes which record.
This page gives row action buttons names that carry both the action and the target. It belongs to accessible names & descriptions for data widgets.
Spec reference
Permalink to "Spec reference"A <button> gets its accessible name from its content (text, and alt or <title> of content images/SVG), or from aria-labelledby, or from aria-label, in precedence order reversed (labelledby wins). An inline SVG icon with no title element contributes nothing; with aria-hidden="true" it is ignored entirely, which is what you want for decorative icons once the button has a text name.
aria-labelledby may reference the button itself and other elements: aria-labelledby="btn-1 row-1-header" produces “Delete INV-1042” from a button whose visible content is “Delete” (hidden) and the row header’s text.
Criteria: SC 4.1.2 Name, Role, Value (a name exists), SC 2.4.6 Headings and Labels and SC 2.4.4 Link Purpose (for icon links) — the name describes the purpose; SC 2.5.3 Label in Name — if the button has a visible text label or a visible tooltip that acts as one, the name should begin with it; SC 1.1.1 Non-text Content — the icon’s meaning is conveyed in text.
When to use each naming technique
Permalink to "When to use each naming technique"Visually hidden text inside the button is the simplest and most robust: “Edit” plus a hidden " INV-1042", or a fully hidden “Edit INV-1042”. It works in every browser and survives translation tools.
aria-labelledby referencing the button and the row header avoids duplicating the record identifier in the DOM and stays correct if the identifier is edited. It is well supported but requires unique ids on every row.
aria-label is fine for a static, fully known name, and convenient in frameworks: aria-label={Edit ${row.id}}. It is invisible text, so check it against the visible tooltip for SC 2.5.3.
The misapplication to name is a title attribute as the only name. title is used as a last-resort name by browsers, but it is inconsistently read, invisible to keyboard users (no hover), and cannot be reached on touch devices.
Annotated code example
Permalink to "Annotated code example"<tr>
<th scope="row" id="r1042">INV-1042</th>
<td>Northwind</td>
<td>1,280.00</td>
<td class="actions">
<!-- Technique 1: visible-to-AT text inside the button -->
<button type="button" class="icon-btn">
<span class="icon icon-edit" aria-hidden="true"></span> <!-- SC 1.1.1 -->
<span class="visually-hidden">Edit INV-1042</span> <!-- SC 4.1.2 -->
</button>
<!-- Technique 2: aria-labelledby = own text + row header -->
<button type="button" class="icon-btn" id="dup1042" aria-labelledby="dup1042 r1042">
<span class="icon icon-copy" aria-hidden="true"></span>
<span class="visually-hidden">Duplicate</span> <!-- name: "Duplicate INV-1042" -->
</button>
<!-- Technique 3: aria-label, generated -->
<button type="button" class="icon-btn danger" aria-label="Delete INV-1042"
data-tooltip="Delete"> <!-- SC 2.5.3: name starts with tooltip -->
<span class="icon icon-bin" aria-hidden="true"></span>
</button>
</td>
</tr>
// React: one component that cannot be rendered without a target
function RowAction({ icon: Icon, action, target, ...props }) {
return (
<button type="button" className="icon-btn" data-tooltip={action} {...props}>
<Icon aria-hidden="true" focusable="false" />
<span className="visually-hidden">{`${action} ${target}`}</span>
</button>
);
}
// <RowAction icon={EditIcon} action="Edit" target={row.id} onClick={…} />
focusable="false" on inline SVG stops old versions of Internet Explorer and some embedded web views from putting the icon into the tab order. It is harmless elsewhere and still recommended in design-system icon components.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | Failure indicator |
|---|---|---|
Tab to edit icon |
“Edit INV-1042, button” | “button” (no name) or “Edit, button” on every row |
NVDA elements list (Insert+F7, Buttons) |
Unique entries per row | Forty identical “Delete” entries |
| VoiceOver rotor, Buttons | “Delete INV-1042” | “trash” or a file name from the SVG |
| Voice control: “Click Delete” | Numbers overlay on all Delete buttons, then choose | Nothing matches if the name does not contain “Delete” |
| Hover or focus tooltip | “Delete” visible | Tooltip text not in the accessible name |
Integration context
Permalink to "Integration context"A three-dot menu button follows the same rule — “Actions for INV-1042” — and then opens a menu whose items can be short (“Edit”, “Delete”) because the menu itself is named for the record; see row action menus with the menu button pattern.
Destructive buttons benefit most from specific names, because the confirmation that follows should repeat the target: “Delete INV-1042? This cannot be undone.” — as in confirming and undoing destructive row actions.
Gotchas
Permalink to "Gotchas"Icon fonts. Icon fonts put private-use characters into the button’s text; some readers announce them as garbage or “unknown”. Wrap the glyph in aria-hidden="true" like an SVG.
Name drift in translations. Visually hidden text is translated with the page; aria-label strings may be missed by translation pipelines that only extract visible text. Prefer hidden text in localised products.
Row identifiers that are not unique. If two rows share a customer name, “Edit Northwind” is ambiguous. Use the real identifier, or combine fields.
Design system notes
Permalink to "Design system notes"The icon button is the component where naming is won or lost for the whole product. Make its label prop required and typed as a string, render the icon inside it as decorative, and derive the visible tooltip from the first word or phrase of the label so the two cannot disagree. For row actions, a wrapper that takes the action and the row label separately — and composes “Edit INV-1042” itself — removes the most common mistake entirely: engineers passing the action alone.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"What should an icon-only delete button in a table row be called?
The action and the record, for example “Delete INV-1042”. A bare “Delete” is ambiguous in a list of every button on the page, which is how many screen reader users navigate.
Is aria-label or visually hidden text better for row buttons?
Both work. Visually hidden text is translated with the page and supported everywhere; aria-label is convenient in components. Whichever you use, the name must include the record and begin with any visible tooltip text.
Should the SVG icon have a title?
Not when the button already has a text name. Hide the icon with aria-hidden=“true” and focusable=“false” so it does not add a second, often unhelpful, name.
Is a title attribute enough to name an icon button?
No. It is a last-resort name that is read inconsistently, only visible on mouse hover, and unavailable to touch and keyboard users as a visible label. Use hidden text or aria-label.
Related
Permalink to "Related"- Row action menus — the kebab menu button
- aria-labelledby for grids — the same id technique
- Confirming destructive actions — what the Delete button leads to