Native-First & ARIA

ARIA gives names, roles, and state to things the browser cannot infer on its own. But native HTML elements already carry their role and keyboard behavior built in, so the first rule of ARIA is: reach for a native element first, and use ARIA only for what HTML cannot express.

HTML7 min readConcept 34 of 42

What ARIA is

ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that modify what the browser communicates to assistive technology through the accessibility tree. There are three kinds: roles (role="dialog"), properties (aria-label="Close"), and states (aria-expanded="true"). ARIA does not add keyboard behavior, visual styles, or DOM structure. It only changes what AT hears. If you add role="button" to a <div>, the AT hears "button" but Tab still skips the element and Enter still does nothing. You have added the label without the substance.

The W3C First Rule of ARIA is: if you can use a native HTML element or attribute with the required semantics and behavior already built in, do so. A <button> is focusable with Tab, activatable with Enter and Space, and communicates role="button" automatically. A <div role="button"> needs tabindex="0", a keydown handler for Enter and Space, an onclick handler, and every ARIA state wired up manually. Native HTML buys you all of that for free.

Why it matters

WCAG 4.1.2 (Name, Role, Value, Level A) requires that every user interface component exposes its name, its role, and any state or property that changes over time. Native HTML satisfies this automatically for interactive elements. ARIA lets you satisfy it for custom widget patterns that have no native equivalent: a tab panel, an autocomplete listbox, a tree view, a custom toggle button that is not a <button> for layout reasons.

Getting ARIA wrong is worse than using no ARIA at all. Incorrect roles confuse AT users who navigate by landmark or widget type. Stale state attributes (an aria-expanded that never updates from false to true) give false information. An aria-hidden="true" on a focused element removes it from the AT view while keyboard users can still reach it, creating a ghost interaction. The accessibility tree tells you what AT actually hears and is the ground truth for auditing ARIA.

How to apply each ARIA category

**Roles** fill widget gaps. Use role="tab" + role="tabpanel" for tab interfaces, role="dialog" for a custom modal (paired with aria-modal="true" and focus trapping), role="alert" for urgent messages, role="tooltip" for hover descriptions. Do not override native roles: <button role="link"> is contradictory.

**Properties** name and describe. Use aria-label for controls with no visible text label (an icon-only close button: aria-label="Close"). Prefer aria-labelledby when a visible heading already names the region, because the label stays in sync with visible text automatically. Use aria-describedby for supplemental hint text (a password requirement note tied to a password input). Properties do not change during interaction.

**States** reflect live conditions. Add aria-expanded="false" to an accordion trigger button and flip it to true with JavaScript when the panel opens. Add aria-pressed to a toggle button. Add aria-checked to a custom checkbox. If you do not update the state with JavaScript on every interaction, AT users see a permanently static state that does not match reality.

For dynamic content announcements, use role="status" (equivalent to aria-live="polite") for non-urgent updates (form saved, item added to cart). Use role="alert" (equivalent to aria-live="assertive") only for errors or urgent messages. Assertive live regions interrupt whatever AT is currently reading, so overusing them is disruptive.

Try it

See what a native <button> provides for free versus what a <div role="button"> still needs, and watch the three ARIA attribute categories in action.

Native element vs ARIA role alone

<button>Click me</button>

  • +In tab orderfree
  • +Enter / Space activatesfree
  • +Focus ring (browser)free
  • +role = buttonfree
  • +Disabled statefree

Zero extra attributes needed. Works with keyboard and AT out of the box.

<div role="button">Click me</div>

  • !In tab orderadd manually
  • !Enter / Space activatesadd manually
  • !Focus ring (browser)add manually
  • +role = buttonfrom role
  • !Disabled stateadd manually

Needs tabindex="0", keydown handler for Enter + Space, and manual state updates.

The three ARIA attribute categories

Roles

Widget patterns HTML has no element for

role="tab"role="dialog"role="tooltip"role="alert"role="tree"

Properties

Stable names and descriptions

aria-labelaria-labelledbyaria-describedbyaria-requiredaria-controls

States

Change with user interaction

aria-expandedaria-checkedaria-pressedaria-selectedaria-hidden

Live regions: polite vs assertive

role="status"aria-live="polite"

Use when

Non-urgent updates

Behavior

Waits until AT finishes the current sentence before announcing

AT announces:

"3 results found"

After a search completes

role="alert"aria-live="assertive"

Use when

Errors and urgent messages only

Behavior

Interrupts whatever AT is currently reading immediately

AT announces:

"Error: email is required"

After a failed form submit

Caution: assertive live regions interrupt AT mid-sentence. Reserve role="alert" for genuine errors and time-sensitive messages only.

Check yourself

Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.

  1. 1A <div role="button" onclick="..."> is clickable. What is still missing for full accessibility?
  2. 2A dialog has a visible h2 heading. Should you use aria-label or aria-labelledby to name it?
  3. 3What is wrong with applying aria-hidden="true" to a focused <button>?

Remember this

  • Use native HTML elements first. <button> gives keyboard support, focus, and role="button" for free.
  • <div role="button"> requires tabindex="0", Enter/Space keydown handlers, and every state update: all manual.
  • Prefer aria-labelledby over aria-label when a visible text element already names the widget.
  • Never put aria-hidden="true" on a focusable element: AT hides it but keyboard users can still reach it.
  • role="status" = polite live region; role="alert" = assertive (interrupts AT immediately, use only for errors).

Done with this concept?

Mark it complete to track your progress. No login needed.