Use aria-label only when an element has no visible text that names it, such as an icon-only button: <button aria-label="Close dialog">. If visible text already names it, rely on that or point aria-labelledby at it. aria-describedby adds extra help that is read after the name. Native HTML labels always come first.
| Situation | Use |
|---|---|
| Visible text inside the element | Nothing extra, the text is the name |
| Form field with a visible caption | <label for> |
| Name already shown elsewhere on screen | aria-labelledby="id" |
| No visible text at all (icon only) | aria-label="..." |
| Extra hint or error after the name | aria-describedby="id" |
Updated
Every button, link, field and landmark needs an accessible name, the short phrase assistive technology speaks when focus lands there. This page shows how that name is worked out, twenty broken patterns next to their repaired versions, and a checker that reads a pasted snippet and reports the name it would get.
Work down this list and stop at the first option that fits. Visible text is first because it serves sighted, screen reader and voice control users with one string.
| Option | What it does | Use it when | Watch out for |
|---|---|---|---|
| Visible text or a <label> | Native name from content or the label element | Almost always, for buttons, links and fields | Placeholder is not a label |
| aria-labelledby | Builds the name from other elements by id | A heading or caption already on screen names the thing | Referenced ids must exist and be unique |
| aria-label | Sets the name as a string, overriding content | There is no visible text, as with icon buttons | Ignored on generic div and span; often not translated |
| aria-describedby | Adds a description read after the name | Hints, format rules, error messages | Not a name; may be skipped by users who turn descriptions off |
Each snippet shows the broken pattern, the repair, and roughly what a screen reader says after the repair. Announcements vary by product and settings.
Without a name the button is typically announced as just button. Hide the drawing so it is not read as an unlabelled image. After the fix it is typically announced as: Close, button.
<!-- Before -->
<button><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
<!-- After -->
<button type="button" aria-label="Close">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>An empty link is often read as its URL, letter by letter on some setups. After the fix it is typically announced as: Our shop on Instagram, link.
<!-- Before -->
<a href="https://example.com/ourshop"><i class="icon-insta"></i></a>
<!-- After -->
<a href="https://example.com/ourshop" aria-label="Our shop on Instagram">
<i class="icon-insta" aria-hidden="true"></i>
</a>Real text in a visually hidden span survives translation tools, which tend to skip attribute values. After the fix it is typically announced as: Cart, 3 items, link.
<!-- Before -->
<a href="/cart"><svg aria-hidden="true">...</svg></a>
<!-- After -->
<a href="/cart">
<svg aria-hidden="true" focusable="false">...</svg>
<span class="visually-hidden">Cart, 3 items</span>
</a>With two unnamed nav elements, the landmark list shows navigation twice and nobody can tell them apart. After the fix it is typically announced as: Main, navigation.
<!-- Before -->
<nav>...main menu...</nav>
<nav>...footer links...</nav>
<!-- After -->
<nav aria-label="Main">...</nav>
<nav aria-label="Footer">...</nav>Placeholder text is a weak fallback name and disappears once someone types. After the fix it is typically announced as: Search products, search edit text.
<!-- Before -->
<input type="search" placeholder="Search">
<!-- After -->
<form role="search">
<label for="q" class="visually-hidden">Search products</label>
<input id="q" type="search" placeholder="e.g. walking boots">
<button>Search</button>
</form>A real label element also makes the caption clickable, which enlarges the target. After the fix it is typically announced as: Email, edit text.
<!-- Before -->
<span>Email</span>
<input type="email" aria-label="Email">
<!-- After -->
<label for="email">Email</label>
<input id="email" type="email" autocomplete="email">Descriptions are joined in the order the ids are listed. After the fix it is typically announced as: Password, secure edit text, invalid entry, At least 12 characters, Too short....
<!-- Before -->
<label for="pw">Password</label>
<input id="pw" type="password">
<p>At least 12 characters</p>
<!-- After -->
<label for="pw">Password</label>
<input id="pw" type="password" aria-describedby="pw-hint pw-err" aria-invalid="true">
<p id="pw-hint">At least 12 characters</p>
<p id="pw-err">Too short, add 4 more characters</p>The legend names the group, so each option makes sense on its own. After the fix it is typically announced as: Delivery speed, group, Standard, radio button, 1 of 2.
<!-- Before -->
<p>Delivery</p>
<input type="radio" id="std" name="d"><label for="std">Standard</label>
<!-- After -->
<fieldset>
<legend>Delivery speed</legend>
<input type="radio" id="std" name="d"><label for="std">Standard</label>
<input type="radio" id="exp" name="d"><label for="exp">Express</label>
</fieldset>Pointing at the visible heading keeps the spoken and shown titles identical. Open it with showModal() for a true modal. After the fix it is typically announced as: Delete project?, dialog, This removes all 14 pages..
<!-- Before -->
<div class="modal">
<h2>Delete project?</h2>...
</div>
<!-- After -->
<dialog aria-labelledby="dlg-t" aria-describedby="dlg-d">
<h2 id="dlg-t">Delete project?</h2>
<p id="dlg-d">This removes all 14 pages.</p>
<button>Cancel</button> <button>Delete</button>
</dialog>Keep the name fixed and let the state change. Do not also swap the label to Unbold. After the fix it is typically announced as: Bold, toggle button, pressed.
<!-- Before -->
<button class="on">Bold</button>
<!-- After -->
<button type="button" aria-pressed="true">Bold</button>Changing both the label and a pressed state at once reads as a double negative. After the fix it is typically announced as: Mute, toggle button, not pressed.
<!-- Before -->
<button aria-label="Unmute"><svg>...</svg></button>
<!-- After -->
<button type="button" aria-label="Mute" aria-pressed="false">
<svg aria-hidden="true" focusable="false">...</svg>
</button>Script must flip aria-expanded and the hidden attribute together. After the fix it is typically announced as: Products, button, collapsed.
<!-- Before -->
<button onclick="toggle()">Products</button>
<ul class="menu">...</ul>
<!-- After -->
<button type="button" aria-expanded="false" aria-controls="prod-menu">Products</button>
<ul id="prod-menu" hidden>...</ul>A clickable div is not focusable or announced as a control at all. After the fix it is typically announced as: Menu, button, collapsed.
<!-- Before -->
<div class="burger" onclick="open()">☰</div>
<!-- After -->
<button type="button" aria-label="Menu" aria-expanded="false" aria-controls="site-nav">
<span aria-hidden="true">☰</span>
</button>Write what the picture tells the reader. Empty alt marks decoration so it is skipped. After the fix it is typically announced as: Sales rose from 40 to 65 orders a week in May, image.
<!-- Before -->
<img src="chart.png" aria-label="chart">
<img src="swirl.png">
<!-- After -->
<img src="chart.png" alt="Sales rose from 40 to 65 orders a week in May">
<img src="swirl.png" alt="">role=img makes the SVG one object instead of a tree of shapes. After the fix it is typically announced as: Northgate Bakery, image.
<!-- Before -->
<svg viewBox="0 0 24 24">...</svg>
<!-- After -->
<svg role="img" aria-labelledby="logo-t" viewBox="0 0 24 24">
<title id="logo-t">Northgate Bakery</title>
...
</svg>A visible caption names the table for everyone. Use aria-labelledby only when the title sits outside it. After the fix it is typically announced as: Opening hours, table, 2 columns, 8 rows.
<!-- Before -->
<table aria-label="table">...</table>
<!-- After -->
<table>
<caption>Opening hours</caption>
<tr><th scope="col">Day</th><th scope="col">Hours</th></tr>
...
</table>The region must exist before its text changes, or many readers miss the update. After the fix it is typically announced as: Saved (spoken without moving focus).
<!-- Before -->
<div class="toast">Saved</div>
<!-- After -->
<div role="status">Saved</div>
<!-- keep this element on the page, then change its text -->aria-roledescription changes the spoken role. Use it sparingly and never on generic elements. After the fix it is typically announced as: Customer photos, carousel.
<!-- Before -->
<div class="carousel">...</div>
<!-- After -->
<section aria-roledescription="carousel" aria-label="Customer photos">
...
</section>Unique link text helps everyone scanning a list of links. Starting with the visible words keeps Label in Name intact. After the fix it is typically announced as: Read more about winter opening hours, link.
<!-- Before -->
<a href="/post/1">Read more</a>
<a href="/post/2">Read more</a>
<!-- After -->
<a href="/post/1" aria-describedby="t1">Read more</a>
<!-- or better: -->
<a href="/post/1">Read more<span class="visually-hidden"> about winter opening hours</span></a>Someone saying click Send to voice control must hit this button, so the visible word belongs in the name. After the fix it is typically announced as: Send, button.
<!-- Before -->
<button aria-label="Submit form">Send</button>
<!-- After -->
<button>Send</button>
<!-- if more context is needed: -->
<button aria-label="Send message">Send</button>| Mistake | Why it hurts | Fix |
|---|---|---|
| aria-label on a div or span with no role | Prohibited, so most readers say nothing | Use a button, link or landmark, or give it a suitable role |
| aria-label that differs from the visible words | Voice control commands miss it | Begin the name with the visible text |
| Role word inside the name, as in Search button | Role is spoken twice | Name it Search |
| aria-hidden="true" on a focusable element | Focus lands on something silent | Remove it, or make the element unfocusable too |
| aria-labelledby pointing at a missing id | Name falls back or ends up empty | Check ids after every template change |
| Same label on every Delete button in a list | Users cannot tell which row it affects | Delete invoice 1042, or aria-labelledby with the row title |
| Using title as the only name | Unreliable for touch and keyboard users | Visible text or aria-label |
| Check | How |
|---|---|
| Every control has a name | Browser dev tools Accessibility pane shows the computed name |
| Names match what is shown | Read the visible label, compare with the computed name |
| Landmarks are distinguishable | Open the landmarks list in a screen reader rotor or elements list |
| States update | Toggle a menu or button and listen for expanded or pressed |
| Live messages are heard | Trigger a save or error without moving focus |
| Real screen reader pass | NVDA or JAWS with Firefox or Chrome on Windows, VoiceOver with Safari on Mac and iPhone, TalkBack on Android |
The W3C guidance on using ARIA opens with one rule: if a native HTML element already has the meaning and behaviour you need, use it instead of adding ARIA to something else. A real button with real text needs no ARIA at all.
ARIA changes only what assistive technology is told. It does not add keyboard support, focus or click handling, so a div with role="button" still needs tabindex and key handlers that a button gives you for free.
The Accessible Name and Description Computation spec sets an order. aria-labelledby comes first, then aria-label, then the native source such as a label element, alt text or the text inside a button or link, and finally the title attribute as a last resort.
Because the first source found wins, a stray aria-label can silently hide a perfectly good visible label. When a name sounds wrong, check the higher-priority attributes before editing the text.
A screen reader typically speaks the name, then the role, then any state such as pressed or expanded, then the description. Exact wording and order differ between products, versions and verbosity settings, so the announcements on this page are approximate.
aria-label holds the name as a string inside the attribute. aria-labelledby points at one or more element ids and builds the name from their text, so the name always matches what sighted users see. When both are present, aria-labelledby wins.
Yes. For most controls the aria-label value overrides the text content, so a button showing Buy that carries aria-label="Add to basket" is announced as Add to basket. That mismatch confuses voice control users and breaks WCAG 2.5.3 Label in Name.
Use it for supporting information that is not the name: a password rule, a format hint or an error message. Screen readers usually read the description after the name and role, often after a short pause, and some users turn descriptions off.
Only if the div has a role that allows naming, such as role="region" or role="dialog". On a generic div or span, ARIA prohibits naming and most screen readers ignore the attribute.
No. Images take their name from the alt attribute. Use alt="" for decoration so the image is skipped. aria-label on an img works but adds nothing alt cannot do.
Related: HTML accessibility checker, Website accessibility checklist, WCAG 2.2 checklist, Color contrast checker, HTML links guide.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.