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.

Row actions and the names behind them Mock table rows ending in edit, duplicate and delete icon buttons, with numbered notes showing each button's accessible name including the record identifier. Row actions and the names behind themInvoiceCustomerAmountActionsINV-1042Northwind1,280.00✎ ⧉ ✕1INV-1043Contoso90.00✎ ⧉ ✕21"Edit INV-1042", "Duplicate INV-1042","Delete INV-1042"2"Edit INV-1043" … — unique names in thebuttons list3Icons are aria-hidden; the tooltip reads"Edit", matching the start of the name
The icon is the same on every row; the name is different on every row.

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
How a row button's name is computed Flow of the accessible name computation for an icon button that uses aria-labelledby referencing itself and the row header. How a row button's name is computedaria-labelledby"dup1042 r1042"Own content"Duplicate"(hidden span)Row header"INV-1042"Iconaria-hidden:ignoredName"DuplicateINV-1042"
With aria-labelledby, the button reads its own hidden text, then borrows the row's identifier.

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.

Names that help and names that do not Comparison of weak accessible names for row icon buttons against strong names that include the action and the record. Names that help and names that do not✗ Weak names(none) — "button""trash-can-outline" — the icon file name"Delete" — identical on every row"Click to delete this item" — no target✓ Strong names"Delete INV-1042""Edit invoice INV-1042""Actions for INV-1042"Starts with the visible tooltip text
A name is complete when it would make sense read out of context, in a list of every button on the page.

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.

Permalink to "Related"

← Back to Accessible Names & Descriptions for Data Widgets