I thought I had seen every way a checkout or a profile page could break. Race conditions in WooCommerce, transient collisions that made no sense at all, all of it. Then last month a button humbled me. The component passed every automated audit, keyboard navigation looked fine to me, and a screen reader user still could not trigger it. That was my introduction to the first rule of ARIA.
On paper it all looked right. The roles were there, the markup was clean, nothing was deprecated. The component just did not behave the way anyone would expect. I had been using ARIA roles as a shortcut for styling and routing, and that is a good way to break the experience without noticing.
Where it went wrong
The design called for a native <button> that looked and behaved like a link. A CSS modifier class would have covered it. Instead I reached for the ARIA role attribute as a styling hook, which felt like fewer moving parts at the time.
<!-- The "Bad" Code that broke accessibility -->
<button class="cta" role="link">Save changes</button>
From the outside nothing looked broken. But giving a <button> a role="link" tells the browser to drop the native button behavior. The Space key stops firing the action reliably, and screen readers get handed two conflicting roles for one element. The browser wins that argument every time.
So what is the first rule of ARIA?
The W3C documentation puts the first rule of ARIA bluntly: Don’t use it. Or more precisely, don’t use it when a native HTML element already has the behavior built in.
A native element hands you all of this without a line of extra code:
- Keyboard activation with
EnterandSpacekeys. - Correct focus management and focus rings.
- Predictable announcements for assistive technology.
- Platform-specific quirks handled by the browser engine.
Putting role="link" on my button threw those defaults away and made me responsible for the whole interaction, keyboard handling included. That is a steep price for a styling shortcut. For the platform side of this, I wrote about the WordPress accessibility improvements landing in core.
The fix was a deletion
I did not add code to fix this. I removed the role attribute, went back to the plain semantic element, and moved the alternative styling into a CSS class, which is where it should have been in the first place.
<!-- The Correct, Semantic Approach -->
<button class="cta cta-alt">Save changes</button>
That also keeps styling and behavior in separate places. ARIA attributes make terrible CSS hooks, because the next person to touch the styling has no reason to suspect they are also changing how the element gets announced. Styles belong in the stylesheet and semantics in the HTML.
Where ARIA actually belongs
None of this makes ARIA useless. It is how you express state. A disclosure widget or a mobile menu has to tell assistive technology whether its panel is open, and plain HTML has no way to say that. You are not overriding what the element is, you are adding something the element cannot say on its own.
<!-- Using ARIA to communicate state, not redefine roles -->
<button id="toggle" aria-expanded="false" aria-controls="panel">
Menu
</button>
<script>
const bbioon_btn = document.getElementById("toggle");
bbioon_btn.addEventListener("click", () => {
const isExpanded = bbioon_btn.getAttribute("aria-expanded") === "true";
bbioon_btn.setAttribute("aria-expanded", !isExpanded);
});
</script>
The browser still treats it as a button, and the screen reader knows whether the content below is showing. That is ARIA adding information rather than arguing with the element.
If accessibility work is eating your sprint, that is the kind of cleanup I take on. I have been building on WordPress since the 4.x days, so I know which parts of the platform cooperate and which ones push back.
Trust the platform
What I took away from the whole episode is that the people who wrote the browser already solved this. Semantic HTML is not a starting point you graduate from. If your ARIA seems to be doing a lot of work, that usually means you replaced something the browser was handling for free. Delete the attribute and see whether the bug leaves with it.