web3icons

Decorative or informative? Hiding icons from screen readers

A quick test for deciding whether an icon is decorative or informative, how to hide it with alt and aria-hidden, and how to avoid double announcements.

5 min read

Every icon on a page is either telling the user something or decorating something that is already said. Screen reader users should hear the first kind and never hear the second. When teams get this wrong, it goes both ways: a status icon that means "Failed" is silent, while a wallet list reads "MetaMask, MetaMask, button" for every row.

This guide gives you a quick test for sorting icons, the markup for hiding the decorative ones, and a walk through a typical swap screen to show how the test plays out.

The test

Cover the icon with your thumb and ask: does a sighted user lose any information?

  • No. The same meaning is in visible text nearby. The icon is decorative. Hide it.
  • Yes, and the icon is inside a button or link. The icon is what names the control. Give the control a name, and hide the icon itself.
  • Yes, and the icon is not interactive. The icon carries information on its own, such as a status, a chain or a warning. Give it a text alternative.

Two follow-up questions catch the edge cases. Does the icon add something the text doesn't, such as "opens in a new tab"? Then that extra meaning needs text. Is the icon repeated next to every row of a list? Then it's almost certainly decoration, and announcing it fifty times is noise.

How to hide an icon

The method depends on how the icon is rendered:

How the icon is rendered How to hide it
<img> alt="", an empty alt, not a missing one
Inline <svg> aria-hidden="true" on the <svg> element
CSS background image or CSS mask Already hidden; CSS images aren't exposed to assistive technology
Icon font glyph in a <span> or <i> aria-hidden="true" on that element

Icon fonts need the attribute even though they look like CSS. The glyph is generated content, often a private-use Unicode character, and pseudo-element content can end up in what a screen reader reads.

You may see role="presentation" or role="none" added to images with empty alt. For an <img>, alt="" already does the same job, so you don't need both.

Here is a token picker button with all three common pieces: a logo, a ticker and a chevron. Only the ticker should be heard, plus the expanded state.

type Props = { symbol: string; logo: string; open: boolean; onClick: () => void }

export function TokenButton({ symbol, logo, open, onClick }: Props) {
  return (
    <button type="button" aria-expanded={open} onClick={onClick}>
      <img src={logo} alt="" width={24} height={24} />
      {symbol}
      <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
        <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.5" />
      </svg>
    </button>
  )
}

A screen reader announces something like "ETH, button, collapsed". The chevron's meaning is carried by aria-expanded, so the drawing itself stays hidden.

Avoiding double announcements

Double announcements happen when an icon and its neighboring text both say the same thing. The wallet list in a connect modal is the classic case:

<button type="button">
  <img src="/img/metamask.svg" alt="MetaMask" width="32" height="32">
  MetaMask
</button>

That button's accessible name is "MetaMask MetaMask". Change the alt to alt="" and the name becomes "MetaMask".

A related mistake is adding aria-label to a control that already has visible text. The label replaces the visible text as the accessible name, so aria-label="Connect wallet" on a button that visibly reads "MetaMask" can stop a voice control user from activating it by saying "click MetaMask". That fails 2.5.3 Label in Name, which requires the accessible name to contain the visible label. If a control has visible text, let the text be the name.

Tooltips are a third source. A title attribute on a decorative <img> or a <title> element left inside an exported SVG can get read as well as shown on hover. Some design tools export a <title> containing the layer name, so you can end up announcing "Group 12" or "icon/eth-24". Strip those titles from production icons, or hide the whole SVG.

Walking through a swap screen

Element Verdict What to do
Product logo in the header, linking home Informative, it names the link Alt text with the product name
Network button showing the chain logo and "Base" Logo decorative alt="", text names the button
Gear icon button with no text Informative, it names the button Button gets a name such as "Swap settings"; icon hidden
ETH logo inside the "You pay" token button Decorative alt=""
Round arrow between the two amount fields Informative, it names the button Name the button "Switch tokens"; icon hidden
Warning triangle before the text "High price impact" Decorative Hide it; the text carries the warning
Warning triangle as the only marker next to "-4.2%" Informative Add text such as "Warning, high price impact"
Small "i" that opens a slippage tooltip Informative, it's a button Name it "About slippage"; icon hidden
Arrow icon after "View on explorer" that opens a new tab Partly informative Hide the icon, add visually hidden text "(opens in new tab)"

For the cases that need extra text, a visually hidden span is usually the most reliable option because it works the same inside buttons, links and plain text:

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

The transaction list is worth a separate look. A green check, a clock or a red cross on its own is informative: it's the only record of whether the transaction succeeded. Pair each with a visible word if you can, and with visually hidden text if you can't. When that status changes while the user is on the page, it also needs announcing; announcing transaction status covers live regions.

Hiding too much

The opposite mistake is quieter and more harmful, because it removes information instead of adding noise.

  • Hiding the only content of a control. If an icon-only button's <svg> has aria-hidden="true" and the button has no other name, the result is a nameless "button". Hiding the icon is correct only once the button has its own name. See icon-only buttons need names.
  • Putting aria-hidden on something focusable. aria-hidden="true" removes an element from the accessibility tree but not from the tab order. A hidden button or a hidden container with links inside leaves keyboard users focused on something their screen reader can't describe.
  • Hiding meaning that has no text twin. Chain badges on token logos, "verified" checkmarks and status dots often look decorative but are the only place that fact appears. Apply the thumb test to each one.

Once an icon is classified as informative and is not inside a control, the next question is how to label it correctly. For <img> that's alt text, covered in alt text for logos and token icons. For inline SVG, see accessible SVG markup.

More in Accessibility

All guides