the spec says: "no aria is better than bad aria." most code ignores this.
Rule 1 of ARIA Use: If you can use a native HTML element with the semantics and behavior you require already built in, do so instead of repurposing an element and adding ARIA.
This is the first rule. It appears first because it is the most important and the most ignored.
When you write <button>, you get: focusability, keyboard activation
(Enter and Space), an implicit role of button, announced type, click
events from keyboard and pointer, and browser default styling.
You get all of this for free. For zero bytes of JavaScript.
When you write <div onclick="...">, you get: a click event.
Nothing else. Every capability you removed must be added back manually,
in JavaScript, that you now have to maintain.
ARIA roles bolted to generic elements. no native list behavior.
ul/ol/li have implicit list and listitem roles. browsers expose them to accessibility trees without any attributes.
instead of
<input aria-required="true" aria-invalid="true">
use
<input required>
aria-required duplicates the required attribute. aria-invalid is set manually after validation.
required adds HTML validation, ARIA semantics, and browser UI. :invalid/:valid CSS pseudoclasses fire automatically. constraint validation API ships with every browser.
focus trap: not implemented. Escape key: not implemented. backdrop: add JS. top-layer: never.
dialog has showModal(). focus trap is automatic. Escape fires cancel event. backdrop is ::backdrop. top-layer means it renders above everything. the div role=dialog has none of this.
when aria is actually needed
ARIA exists for patterns HTML does not cover.
Custom widgets with no native equivalent.
Dynamic state that has no HTML expression.
When you have genuinely exhausted the HTML spec, then reach for ARIA.
custom range slider
input type=range exists but may not match your visual design. if you build a custom one, use role=slider with aria-valuemin, aria-valuemax, aria-valuenow.
tab panel interface
no native tab element. role=tablist, role=tab, role=tabpanel with aria-selected and aria-controls is the correct pattern here.
combobox with custom options
datalist covers simple autocomplete. for complex filtered searches with custom rendering, role=combobox with aria-expanded and aria-activedescendant is appropriate.
live regions
aria-live=polite announces dynamic content changes. status updates, form feedback, loading states. the only way to communicate async changes to screen readers.
tree widget
no native collapsible tree element. role=tree with role=treeitem, aria-expanded, aria-level is the spec. complex. worth it if you need it.
the four rules
The W3C ARIA specification ships with four authoring practices.
All interactive ARIA widgets must be keyboard operable.
Do not change native semantics unless you have no choice.
All interactive ARIA controls must have an accessible name.
No ARIA is better than bad ARIA.
Rule four: no ARIA is better than bad ARIA.
A div with no ARIA is invisible to screen readers but does not break anything.
A div with incorrect ARIA announces false information.
Silence is better than lies.