Implementing Icon Accessibility in Web Apps
Learn practical techniques for making icons accessible in web applications, including ARIA labels, SVG best practices, and screen reader compatibility for inclusive design.
Look, we've been building web apps for years now, and icons are everywhere. They're probably in your navigation, your buttons, your forms, maybe even replacing entire text labels because some designer thought it looked cleaner. But here's the thing that keeps me up at night – most of these icons are completely inaccessible to a huge chunk of users.
I'm talking about people using screen readers, keyboard navigation, or just folks who don't immediately recognize what your fancy minimalist icon means. And honestly? We're kind of failing them. Not because we're bad people, but because icon accessibility isn't as straightforward as it seems like it should be.
![]()
Why Icon Accessibility Actually Matters
So yeah, accessibility is important. We all know this. But let me be real with you – icon accessibility specifically is one of those things that gets overlooked constantly. I've reviewed countless codebases where buttons are just... SVG elements with no labels. Nothing. A screen reader hits that button and announces "button" with zero context about what it does.
Turns out, this isn't just annoying. It's excluding millions of users from using your app properly.
Think about it this way. You've got a search icon – that little magnifying glass everyone uses. You know what it does. I know what it does. But a screen reader user? They just hear "graphic" or "image" or sometimes nothing at all if you haven't set it up right. They're supposed to just... guess? That's not acceptable.
Plus there's the legal angle. The European Accessibility Act, ADA, WCAG guidelines – they're all getting stricter about this stuff. But honestly, I think the legal compliance is the least interesting reason to care. The real reason is that accessible icons just make better products for everyone.
The Two Types of Icons You Need to Understand
Before we dive into implementation, you need to wrap your head around this distinction. There are functional icons and decorative icons. This sounds simple but people mess it up all the time.
![]()
![]()
Functional icons are the ones that do something or convey actual information. Like:
- Icon-only buttons (that hamburger menu, close buttons, edit icons)
- Icons that represent actions (trash can for delete, paper plane for send)
- Icons that are the only label for a link or control
Decorative icons are just there for looks. They don't add information. They're visual candy. Like:
- An icon next to text that already explains everything
- Little graphics that just make the page prettier
- Icons that duplicate information already present
The implementation approach is completely different for these two categories. Get this wrong and you're either overwhelming screen reader users with redundant information or leaving them in the dark about important functionality.
The ARIA Label Approach (My Go-To Method)
Okay, so you've got an icon-only button. Maybe it's a settings gear or a notification bell. Here's probably the cleanest way to handle it:
<button aria-label="Open settings">
<svg aria-hidden="true">
<!-- your icon paths here -->
</svg>
</button>
See what I did there? The button gets an aria-label that describes the action – not the icon itself. Don't write "gear icon" or "settings icon." Write what happens when someone clicks it. "Open settings" or "View settings" or whatever makes sense in your context.
And that aria-hidden="true" on the SVG? That's telling screen readers to completely ignore the graphic element. Because we've already provided the label on the button itself. Without this, some screen readers will announce both the label AND try to describe the SVG, which gets messy fast.
But here's where it gets weird. The aria-label attribute doesn't get translated by browser translation tools. So if someone's using Chrome's auto-translate feature, your carefully crafted label stays in English while the rest of the page is in Spanish or Japanese or whatever. Kind of a bummer, honestly.
The Visually Hidden Text Pattern
This is another approach I see a lot, and it works pretty well:
<button>
<svg aria-hidden="true">
<!-- icon paths -->
</svg>
<span class="visually-hidden">Delete item</span>
</button>
Your CSS for that .visually-hidden class looks something like:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
This technique hides the text visually but keeps it in the DOM for screen readers. Plus, it DOES get picked up by translation tools, which is nice. And if CSS fails to load for some reason, the user still gets the text label. So that's kind of a win.
The downside? It's more markup. And you have to remember to add that class. I've forgotten it more times than I'd like to admit, and then the text just... shows up next to your icon looking weird.
SVG Icons Need Special Care
SVGs are great for icons. They scale perfectly, they're lightweight, you can manipulate them with CSS. But accessibility-wise, they're kind of a pain.
Here's the basic pattern for a standalone SVG icon:
<svg role="img" aria-label="Warning">
<title>Warning</title>
<!-- icon paths -->
</svg>
That <title> element is supposed to provide a tooltip and accessible name. The role="img" tells assistive tech to treat it as an image. And the aria-label reinforces the description.
But honestly? This support is inconsistent across browsers and screen readers. I've tested this on different combinations and gotten different results. So for critical functional icons, I usually wrap them in a button or link and handle the accessibility at that level instead.
For decorative SVGs, just slap on aria-hidden="true" and call it a day:
<svg aria-hidden="true">
<!-- decorative icon paths -->
</svg>
Yeah, it's that simple when the icon doesn't actually matter for functionality.
![]()
Icon Fonts Are Kind of Terrible for Accessibility
I'm just gonna say it. Icon fonts like Font Awesome seemed like a great idea, but they're accessibility nightmares. The icon is rendered as a character from a custom font, which means screen readers try to announce it as text. Sometimes they announce the Unicode character. Sometimes they announce nothing. Sometimes they announce gibberish.
If you're stuck using icon fonts (maybe you inherited a codebase or whatever), here's how to make them less terrible:
<button>
<i class="fa fa-envelope" aria-hidden="true"></i>
<span class="visually-hidden">Send email</span>
</button>
Always hide the font icon with aria-hidden and provide real text. Always. Don't trust that the icon font library has somehow magically solved accessibility. They haven't.
Turns out the people claiming Font Awesome "won't trip up screen readers" were being... optimistic. It absolutely can and does cause problems if you're not careful.
Decorative Icons Done Right
So you've got an icon that's just there for visual appeal. Maybe it's next to a heading, or it's reinforcing text that's already clear. Don't overthink it:
<h2>
<svg aria-hidden="true">
<!-- decorative icon -->
</svg>
Product Features
</h2>
That's it. The heading text says "Product Features" – we don't need the screen reader to also announce whatever icon is sitting there looking pretty.
I see designers get really attached to decorative icons, like they're critical to the experience. They're not. They're nice visual touches. Treat them as optional enhancements and your accessibility improves dramatically.
The Contrast and Size Problem
Here's something that trips people up. Icons need sufficient contrast with their background – at least 3:1 according to WCAG standards. And I mean the icon itself, not just text.
I've seen beautiful minimal designs where the icons are light gray on white. Looks gorgeous in your design mockups on a 4K monitor in a dark room. But try using that on a phone in sunlight, or if you have low vision, or if you're colorblind. It's basically invisible.
Use tools like WebAIM's contrast checker or the Stark plugin for Figma. Just check your contrast. It takes two seconds and saves so much pain later.
And size matters too. WCAG 2.2 says interactive elements should be at least 24x24 pixels. But honestly? I aim for 44x44 pixels minimum, especially for touch targets on mobile. Smaller than that and you're asking people with motor control issues to perform pixel-perfect taps. Not cool.
Interactive Icons Need Keyboard Support
This should be obvious but I've seen it messed up so many times. If you can click an icon, you need to be able to reach it and activate it with a keyboard.
Use semantic HTML. Buttons for actions, links for navigation:
<!-- Good -->
<button onclick="doSomething()">
<svg aria-hidden="true">...</svg>
<span class="visually-hidden">Edit</span>
</button>
<!-- Bad -->
<div onclick="doSomething()">
<svg>...</svg>
</div>
That div isn't focusable by default. It doesn't respond to Enter or Space. Screen readers don't know it's interactive. You'd have to add tabindex, role="button", keyboard event handlers... it's so much extra work. Just use a button.
And make sure your focus indicators are visible. That default browser outline isn't pretty, but don't just remove it with outline: none and call it a day. Design a custom focus style that's actually visible:
button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
![]()
Tooltips and Additional Context
Sometimes an icon needs more explanation than a simple label can provide. That's where aria-describedby comes in:
<button aria-label="Delete" aria-describedby="delete-help">
<svg aria-hidden="true">...</svg>
</button>
<div id="delete-help" class="tooltip">
This will permanently delete the item
</div>
The aria-describedby creates a relationship between the button and the additional help text. Screen readers announce the label first, then optionally read the description.
But be careful with tooltip visibility. If it only shows on hover, keyboard and touch users might never see it. I usually make tooltips appear on focus as well, and make them dismissable.
The Hamburger Menu Debate
Okay, I have opinions about hamburger menus. Yeah, they're everywhere. Yeah, they save space. But turns out not everyone knows what three horizontal lines mean.
I've watched usability tests where people just... didn't see the menu. Or they saw it but didn't know it was interactive. So what do you do?
Add text. I know, I know, it ruins the minimal aesthetic. But "Menu" next to the hamburger icon makes it instantly clearer:
<button aria-label="Open navigation menu" class="menu-button">
<svg aria-hidden="true">
<!-- hamburger icon -->
</svg>
<span>Menu</span>
</button>
And make sure the aria-label reflects the current state. If the menu is open, it should say "Close navigation menu" instead. Dynamic labels matter.
![]()
Testing Your Icon Accessibility
You can't just implement this stuff and assume it works. You gotta test it. Here's my process:
Test with a screen reader. NVDA on Windows is free. VoiceOver on Mac is built-in. Turn it on and actually navigate your app. Does every icon make sense? Can you tell what actions will happen?
Test with keyboard only. Unplug your mouse (or just don't touch it). Navigate using Tab, Shift+Tab, Enter, Space. Can you reach every interactive icon? Is the focus order logical?
Test the contrast. Use browser dev tools or online checkers. Do your icons meet 3:1 contrast minimum?
Test on mobile. Are the touch targets big enough? Can you tap them reliably without hitting the wrong thing?
I try to do this kind of testing throughout development, not just at the end. Turns out fixing accessibility issues is way easier when you catch them early.
Common Mistakes I See Everywhere
Let me just rant for a minute about things that drive me crazy:
Using icons without any accessible name. This is the big one. Just an SVG or icon font character sitting there naked. Screen readers have no idea what to do with it.
Hiding icons from screen readers when they're functional. Putting aria-hidden="true" on something that actually needs to be announced. This makes the button or link completely invisible to assistive tech.
Inconsistent icon usage. Using the same icon to mean different things in different contexts. Or using different icons for the same action. Pick your patterns and stick to them.
Relying only on color to convey information. Like using a red icon to indicate an error but nothing else. Color blind users might miss it entirely.
Making icons too small. I get that space is precious, but 16x16 pixel interactive icons on mobile are basically unusable for a lot of people.
Context-Specific Patterns
Different situations need different approaches. Let me break down some common scenarios:
Social Media Links
<a href="https://twitter.com/yourhandle" aria-label="Follow us on Twitter">
<svg aria-hidden="true">
<!-- Twitter bird icon -->
</svg>
</a>
The aria-label should say what happens, not just "Twitter." Makes sense when you're navigating by links with a screen reader.
Form Field Icons
<label for="search">
Search
<input id="search" type="search" />
</label>
<button type="submit" aria-label="Submit search">
<svg aria-hidden="true">
<!-- magnifying glass icon -->
</svg>
</button>
The icon on the button needs its own label because it's a separate interactive element. The form field already has its label element, so we're good there.
Status Indicators
<div role="status" aria-live="polite">
<svg aria-hidden="true">
<!-- checkmark icon -->
</svg>
<span>Settings saved successfully</span>
</div>
The text conveys the status, so the icon is decorative. But we use role="status" and aria-live to make sure screen readers announce the update.
Icon Buttons in Tables
Tables are tricky because you need context. "Edit" or "Delete" by itself isn't enough – edit what?
<button aria-label="Edit John Smith's profile">
<svg aria-hidden="true">
<!-- edit icon -->
</svg>
</button>
Include the row context in the label. Yeah, it's more verbose, but it's so much clearer for screen reader users navigating table by table.
![]()
The Icon Library Question
Should you use a pre-made icon library or create custom icons? Honestly, both work if you implement accessibility correctly.
Libraries like Heroicons, Lucide, or Material Icons give you consistent, well-designed icons. But you still have to add the accessibility layer yourself. They're just the graphics.
Custom icons give you complete control over the design, but then you're responsible for creating clear, recognizable symbols. And you need to maintain consistency across your app.
I usually go with a library for speed, then customize specific icons that need to match our brand. But the accessibility implementation is the same either way – proper labels, appropriate hiding of decorative elements, sufficient contrast and size.
Internationalization Considerations
Icons should be culturally appropriate. Some symbols mean different things in different cultures. A thumbs up is positive in many Western countries but offensive in some Middle Eastern countries. A checkmark means "correct" in most places but means "incorrect" in some East Asian contexts.
Do your research if you're building for a global audience. And remember that text labels (even visually hidden ones) need to be translated along with the rest of your interface.
Plus, reading direction matters. If your app supports RTL languages like Arabic or Hebrew, some icons need to be flipped. Directional arrows, back buttons, that kind of thing.
Dynamic Icons and State Changes
Icons that change based on state need to communicate those changes. Like a bookmark icon that toggles between filled and unfilled:
<button aria-label="Add to bookmarks" aria-pressed="false">
<svg aria-hidden="true">
<!-- empty bookmark icon -->
</svg>
</button>
When clicked and bookmarked:
<button aria-label="Remove from bookmarks" aria-pressed="true">
<svg aria-hidden="true">
<!-- filled bookmark icon -->
</svg>
</button>
The aria-pressed attribute tells assistive tech this is a toggle button and what its current state is. And the label changes to reflect what will happen next.
Animation and Motion
Animated icons are trendy. Loading spinners, hamburger menus that morph into X's, bouncing notifications. But some users have vestibular disorders that make animation physically uncomfortable or disabling.
Respect the prefers-reduced-motion media query:
.icon {
animation: spin 2s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.icon {
animation: none;
}
}
And if an icon is animating to convey information (like a loading state), make sure that information is also available to screen readers through text or ARIA live regions.
My Personal Approach
After years of dealing with this, here's my default workflow:
-
Design phase: Make sure icons are at least 24x24px, preferably 44x44px for interactive elements. Check contrast. Use familiar symbols.
-
Development: Use semantic HTML (button/link). Add aria-label or visually hidden text. Hide decorative icons with aria-hidden.
-
Review: Tab through the interface. Turn on a screen reader. Does everything make sense?
-
Iterate: Fix problems immediately while the code is fresh.
It's not complicated once you build the habit. But it requires being intentional about it. You can't just slap icons everywhere and hope for the best.
The Future of Icon Accessibility
Things are getting better. Browser support for ARIA is more consistent. Design systems are starting to include accessibility in their icon components out of the box. Automated testing tools are catching more issues.
But we're not there yet. It's still on us as developers and designers to implement this correctly. The tools can help, but they can't make the decisions about what an icon means or how it should be labeled.
I think we'll see more AI-powered tools that can suggest appropriate labels or catch missing accessibility attributes. That'll probably help. But ultimately, accessibility is about understanding your users and their needs. No AI can replace that empathy and intentionality.
Wrapping This Up
Icon accessibility isn't some optional nice-to-have feature. It's fundamental to making web apps that everyone can use. And honestly? It's not that hard once you understand the patterns.
Use proper labels. Hide decorative icons. Make interactive elements keyboard accessible. Test with real assistive technology. That's like 90% of it right there.
Yeah, there are edge cases and complex scenarios. But start with the basics and you'll already be way ahead of most web apps out there. Your screen reader users will thank you. Your keyboard users will thank you. And you'll sleep better knowing you're not accidentally excluding people from using your app.
Just do it. Make your icons accessible. It matters.