axe "Buttons must have discernible text"
A button has no text, label, or titled icon, so screen readers announce "button" with no idea what it does.
What you see
Buttons must have discernible text Element does not have inner text that is visible to screen readers <button class="menu-toggle"><svg>…</svg></button>
What’s actually happening
axe flags a <button> (or an element with role="button") that has no accessible name. A screen reader reaches it and says just "button" — no clue whether it submits, deletes, closes, or opens a menu. It turns up most on icon buttons: a hamburger toggle, an X close, a play control, a trash icon, all rendered as an SVG or icon font with nothing else inside. The control still works for a mouse user who recognizes the glyph. For someone navigating by the buttons list, it's a row of anonymous "button, button, button" they can't act on with any confidence.
Common causes
- Icon-only buttons where the SVG or icon-font glyph carries the meaning and no text or label was added.
- A custom control built from a <div> or <span> with role="button" and a click handler, but no aria-label.
- Visible text removed with CSS (text-indent:-9999px, font-size:0) in a way that also dropped it from the accessibility tree.
- A component library button that takes an icon prop but no label prop, so every instance ships nameless.
- Button text that only exists as a CSS ::before pseudo-element, which doesn't count as the accessible name.
How to fix it
- Give icon buttons an aria-label naming the actionState what the button does, not what it looks like: <button aria-label="Open menu"><svg aria-hidden="true">…</svg></button>. Mark the SVG aria-hidden="true" so it isn't read alongside the label. The accessible name should match the action a sighted user infers from the icon — "Close dialog", "Play", "Delete row".
- Or keep visible text and clip itIf you'd rather keep the name in the DOM as text, put it in a visually-hidden span: <button><svg aria-hidden="true">…</svg><span class="sr-only">Search</span></button>. Use an sr-only/clip-path class, not display:none — display:none removes the text from the accessibility tree and you're back to a nameless button.
- Use a real <button>, not a clickable divA <div role="button"> needs a label, tabindex="0", and key handlers for Enter and Space that you wrote by hand. A native <button> gives you focusability, keyboard activation, and the right role for free. Swap the div for a button and most of the work disappears — then just add the label.
- Make the label a required prop on shared icon buttonsIf a component renders an icon button, require the label in its API (e.g. an iconButton that won't compile without a label string). One nameless button in a design system multiplies across every page that uses it; enforcing the prop kills the whole class of failures at the source.
- Re-run axe and confirm what's announcedRe-scan; button-name should clear. Then focus each control with a screen reader and check it speaks the action you intended. The scanner only proves a name exists — only listening proves it's the right name.
Stop it recurring
Require a label prop on every icon-button component and add an axe pass in CI so a button without an accessible name can't merge.